/*
 *    Copyright (c) 2024 Project CHIP Authors
 *    All rights reserved.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
#include <pw_unit_test/framework.h>

#include <data-model-providers/codegen/tests/EmberInvokeOverride.h>
#include <data-model-providers/codegen/tests/EmberReadWriteOverride.h>

#include <access/AccessControl.h>
#include <access/Privilege.h>
#include <access/SubjectDescriptor.h>
#include <app-common/zap-generated/attribute-type.h>
#include <app-common/zap-generated/cluster-objects.h>
#include <app/AttributeAccessInterface.h>
#include <app/AttributeAccessInterfaceRegistry.h>
#include <app/AttributeEncodeState.h>
#include <app/AttributeValueDecoder.h>
#include <app/CommandHandlerInterface.h>
#include <app/CommandHandlerInterfaceRegistry.h>
#include <app/CommandHandlerInterfaceShim.h>
#include <app/ConcreteAttributePath.h>
#include <app/ConcreteClusterPath.h>
#include <app/ConcreteCommandPath.h>
#include <app/GlobalAttributes.h>
#include <app/MessageDef/ReportDataMessage.h>
#include <app/data-model-provider/MetadataLookup.h>
#include <app/data-model-provider/MetadataTypes.h>
#include <app/data-model-provider/OperationTypes.h>
#include <app/data-model-provider/StringBuilderAdapters.h>
#include <app/data-model-provider/tests/ReadTesting.h>
#include <app/data-model-provider/tests/TestConstants.h>
#include <app/data-model-provider/tests/WriteTesting.h>
#include <app/data-model/Decode.h>
#include <app/data-model/Encode.h>
#include <app/data-model/List.h>
#include <app/data-model/Nullable.h>
#include <app/server-cluster/DefaultServerCluster.h>
#include <app/server-cluster/testing/TestEventGenerator.h>
#include <app/server-cluster/testing/TestServerClusterContext.h>
#include <app/util/attribute-metadata.h>
#include <app/util/attribute-storage-null-handling.h>
#include <app/util/ember-io-storage.h>
#include <app/util/mock/Constants.h>
#include <app/util/mock/Functions.h>
#include <app/util/mock/MockNodeConfig.h>
#include <app/util/odd-sized-integers.h>
#include <data-model-providers/codegen/CodegenDataModelProvider.h>
#include <lib/core/CHIPError.h>
#include <lib/core/DataModelTypes.h>
#include <lib/core/Optional.h>
#include <lib/core/StringBuilderAdapters.h>
#include <lib/core/TLVDebug.h>
#include <lib/core/TLVReader.h>
#include <lib/core/TLVTags.h>
#include <lib/core/TLVTypes.h>
#include <lib/core/TLVWriter.h>
#include <lib/support/ReadOnlyBuffer.h>
#include <lib/support/Span.h>
#include <lib/support/TestPersistentStorageDelegate.h>
#include <lib/support/tests/ExtraPwTestMacros.h>
#include <protocols/interaction_model/StatusCode.h>

#include <optional>
#include <vector>

using namespace chip;
using namespace chip::Testing;
using namespace chip::app;
using namespace chip::app::Testing;
using namespace chip::app::DataModel;
using namespace chip::app::Clusters::Globals::Attributes;

using chip::Protocols::InteractionModel::Status;

// Mock function for linking
void InitDataModelHandler() {}

namespace {

constexpr EventId kTestEventId = 0x321;

constexpr AttributeId kAttributeIdReadOnly        = 0x3001;
constexpr AttributeId kAttributeIdTimedWrite      = 0x3002;
constexpr AttributeId kAttributeIdFakeAllowsWrite = 0x3003;

constexpr CommandId kMockCommandId1 = 0x1234;
constexpr CommandId kMockCommandId2 = 0x1122;

constexpr EndpointId kEndpointIdThatIsMissing = kMockEndpointMin - 1;

constexpr AttributeId kReadOnlyAttributeId = 0x5001;

constexpr DeviceTypeId kDeviceTypeId1   = 123;
constexpr uint8_t kDeviceTypeId1Version = 10;

constexpr DeviceTypeId kDeviceTypeId2   = 1122;
constexpr uint8_t kDeviceTypeId2Version = 11;

constexpr DeviceTypeId kDeviceTypeId3   = 3;
constexpr uint8_t kDeviceTypeId3Version = 33;

constexpr DeviceTypeId kDeviceTypeId4   = 1123;
constexpr uint8_t kDeviceTypeId4Version = 33;

constexpr uint8_t kNamespaceID1 = 123;
constexpr uint8_t kTag1         = 10;
constexpr char kLabel1[]        = "Label1";

constexpr uint8_t kNamespaceID2 = 254;
constexpr uint8_t kTag2         = 22;
constexpr char kLabel2[]        = "Label2";

constexpr uint8_t kNamespaceID3 = 3;
constexpr uint8_t kTag3         = 32;

static_assert(kEndpointIdThatIsMissing != kInvalidEndpointId);
static_assert(kEndpointIdThatIsMissing != kMockEndpoint1);
static_assert(kEndpointIdThatIsMissing != kMockEndpoint2);
static_assert(kEndpointIdThatIsMissing != kMockEndpoint3);
static_assert(kEndpointIdThatIsMissing != kMockEndpoint4);

bool operator==(const Access::SubjectDescriptor & a, const Access::SubjectDescriptor & b)
{
    if (a.fabricIndex != b.fabricIndex)
    {
        return false;
    }
    if (a.authMode != b.authMode)
    {
        return false;
    }
    if (a.subject != b.subject)
    {
        return false;
    }
    for (unsigned i = 0; i < a.cats.values.size(); i++)
    {
        if (a.cats.values[i] != b.cats.values[i])
        {
            return false;
        }
    }
    return true;
}

struct TestCodegenModelViaMocks : public ::testing::Test
{
    static void SetUpTestSuite() { ASSERT_EQ(chip::Platform::MemoryInit(), CHIP_NO_ERROR); }
    static void TearDownTestSuite() { chip::Platform::MemoryShutdown(); }
};

class TestProviderChangeListener : public ProviderChangeListener
{
public:
    void MarkDirty(const AttributePathParams & path) override { mDirtyList.push_back(path); }

    std::vector<AttributePathParams> & DirtyList() { return mDirtyList; }
    const std::vector<AttributePathParams> & DirtyList() const { return mDirtyList; }

private:
    std::vector<AttributePathParams> mDirtyList;
};

class TestActionContext : public ActionContext
{
public:
    Messaging::ExchangeContext * CurrentExchange() override { return nullptr; }
};

class CodegenDataModelProviderWithContext : public CodegenDataModelProvider
{
public:
    CodegenDataModelProviderWithContext()
    {
        SetPersistentStorageDelegate(&mStorageDelegate);
        EXPECT_SUCCESS(Startup({
            .eventsGenerator         = mEventGenerator,
            .dataModelChangeListener = mChangeListener,
            .actionContext           = mActionContext,
        }));
    }
    ~CodegenDataModelProviderWithContext() override { EXPECT_SUCCESS(Shutdown()); }

    TestProviderChangeListener & ChangeListener() { return mChangeListener; }
    const TestProviderChangeListener & ChangeListener() const { return mChangeListener; }

private:
    LogOnlyEvents mEventGenerator;
    TestProviderChangeListener mChangeListener;
    TestActionContext mActionContext;
    TestPersistentStorageDelegate mStorageDelegate;
};

class MockAccessControl : public Access::AccessControl::Delegate, public Access::AccessControl::DeviceTypeResolver
{
public:
    CHIP_ERROR Check(const Access::SubjectDescriptor & subjectDescriptor, const Access::RequestPath & requestPath,
                     Access::Privilege requestPrivilege) override
    {
        if (subjectDescriptor == kAdminSubjectDescriptor)
        {
            return CHIP_NO_ERROR;
        }
        if ((subjectDescriptor == kViewSubjectDescriptor) && (requestPrivilege == Access::Privilege::kView))
        {
            return CHIP_NO_ERROR;
        }
        return CHIP_ERROR_ACCESS_DENIED;
    }

    bool IsDeviceTypeOnEndpoint(DeviceTypeId deviceType, EndpointId endpoint) override { return true; }
};

class MockCommandHandler : public CommandHandler
{
public:
    CHIP_ERROR FallibleAddStatus(const ConcreteCommandPath & aRequestCommandPath,
                                 const Protocols::InteractionModel::ClusterStatusCode & aStatus,
                                 const char * context = nullptr) override
    {
        // MOCK: do not do anything here
        return CHIP_NO_ERROR;
    }

    void AddStatus(const ConcreteCommandPath & aRequestCommandPath, const Protocols::InteractionModel::ClusterStatusCode & aStatus,
                   const char * context = nullptr) override
    {
        // MOCK: do not do anything here
    }

    FabricIndex GetAccessingFabricIndex() const override { return 1; }

    CHIP_ERROR AddResponseData(const ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId,
                               const DataModel::EncodableToTLV & aEncodable) override
    {
        return CHIP_NO_ERROR;
    }

    void AddResponse(const ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId,
                     const DataModel::EncodableToTLV & aEncodable) override
    {}

    bool IsTimedInvoke() const override { return false; }

    void FlushAcksRightAwayOnSlowCommand() override {}

    Access::SubjectDescriptor GetSubjectDescriptor() const override { return kAdminSubjectDescriptor; }

    Messaging::ExchangeContext * GetExchangeContext() const override { return nullptr; }
};

/// Overrides Enumerate*Commands in the CommandHandlerInterface to allow
/// testing of behaviors when command enumeration is done in the interace.
class CustomListCommandHandler : public CommandHandlerInterface
{
public:
    CustomListCommandHandler(Optional<EndpointId> endpointId, ClusterId clusterId) : CommandHandlerInterface(endpointId, clusterId)
    {
        EXPECT_SUCCESS(CommandHandlerInterfaceRegistry::Instance().RegisterCommandHandler(this));
    }
    ~CustomListCommandHandler() { EXPECT_SUCCESS(CommandHandlerInterfaceRegistry::Instance().UnregisterCommandHandler(this)); }

    void InvokeCommand(HandlerContext & handlerContext) override
    {
        if (mHandleCommand)
        {
            handlerContext.mCommandHandler.AddStatus(handlerContext.mRequestPath, Protocols::InteractionModel::Status::Success);
            handlerContext.SetCommandHandled();
        }
        else
        {
            handlerContext.SetCommandNotHandled();
        }
    }

    void SetHandleCommands(bool handle) { mHandleCommand = handle; }

    CHIP_ERROR RetrieveAcceptedCommands(const ConcreteClusterPath & cluster,
                                        ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> & builder) override
    {
        VerifyOrReturnError(mOverrideAccepted, CHIP_ERROR_NOT_IMPLEMENTED);
        return builder.AppendElements(Span<const AcceptedCommandEntry>(mAccepted.data(), mAccepted.size()));
    }

    CHIP_ERROR RetrieveGeneratedCommands(const ConcreteClusterPath & cluster, ReadOnlyBufferBuilder<CommandId> & builder) override
    {
        VerifyOrReturnError(mOverrideGenerated, CHIP_ERROR_NOT_IMPLEMENTED);
        return builder.AppendElements(Span<const CommandId>(mGenerated.data(), mGenerated.size()));
    }

    void SetOverrideAccepted(bool overrideAccepted) { mOverrideAccepted = overrideAccepted; }
    void SetOverrideGenerated(bool overrideGenerated) { mOverrideGenerated = overrideGenerated; }

    std::vector<DataModel::AcceptedCommandEntry> & AcceptedVec() { return mAccepted; }
    std::vector<CommandId> & GeneratedVec() { return mGenerated; }

private:
    bool mOverrideAccepted  = false;
    bool mOverrideGenerated = false;
    bool mHandleCommand     = false;

    std::vector<DataModel::AcceptedCommandEntry> mAccepted;
    std::vector<CommandId> mGenerated;
};

/// Overrides Enumerate*Commands in the CommandHandlerInterface to allow
/// testing of behaviors when command enumeration is done in the interace.
class ShimCommandHandler : public CommandHandlerInterfaceShim<Clusters::UnitTesting::Id>
{
public:
    ShimCommandHandler(Optional<EndpointId> endpointId, ClusterId clusterId) : CommandHandlerInterfaceShim(endpointId, clusterId)
    {
        EXPECT_SUCCESS(CommandHandlerInterfaceRegistry::Instance().RegisterCommandHandler(this));
    }
    ~ShimCommandHandler() { EXPECT_SUCCESS(CommandHandlerInterfaceRegistry::Instance().UnregisterCommandHandler(this)); }

    void InvokeCommand(HandlerContext & handlerContext) override
    {
        if (mHandleCommand)
        {
            handlerContext.mCommandHandler.AddStatus(handlerContext.mRequestPath, Protocols::InteractionModel::Status::Success);
            handlerContext.SetCommandHandled();
        }
        else
        {
            handlerContext.SetCommandNotHandled();
        }
    }

    //
    void SetHandleCommands(bool handle) { mHandleCommand = handle; }

    //
    CHIP_ERROR EnumerateAcceptedCommands(const ConcreteClusterPath & cluster, CommandIdCallback callback, void * context) override
    {
        VerifyOrReturnError(mOverrideAccepted, CHIP_ERROR_NOT_IMPLEMENTED);

        for (auto id : mAccepted)
        {
            if (callback(id, context) != Loop::Continue)
            {
                break;
            }
        }
        return CHIP_NO_ERROR;
    }

    //
    CHIP_ERROR EnumerateGeneratedCommands(const ConcreteClusterPath & cluster, CommandIdCallback callback, void * context) override
    {
        VerifyOrReturnError(mOverrideGenerated, CHIP_ERROR_NOT_IMPLEMENTED);

        for (auto id : mGenerated)
        {
            if (callback(id, context) != Loop::Continue)
            {
                break;
            }
        }
        return CHIP_NO_ERROR;
    }

    void SetOverrideAccepted(bool overrideAccepted) { mOverrideAccepted = overrideAccepted; }
    void SetOverrideGenerated(bool overrideGenerated) { mOverrideGenerated = overrideGenerated; }

    std::vector<CommandId> & AcceptedVec() { return mAccepted; }
    std::vector<CommandId> & GeneratedVec() { return mGenerated; }

private:
    bool mOverrideAccepted  = false;
    bool mOverrideGenerated = false;
    bool mHandleCommand     = false;

    std::vector<CommandId> mAccepted;
    std::vector<CommandId> mGenerated;
};

class ScopedMockAccessControl
{
public:
    ScopedMockAccessControl()
    { // We are sometimes initializing twice, resulting in a failure
        TEMPORARY_RETURN_IGNORED Access::GetAccessControl().Init(&mMock, mMock);
    }
    ~ScopedMockAccessControl() { Access::GetAccessControl().Finish(); }

private:
    MockAccessControl mMock;
};

#define MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(zcl_type) MockAttributeId(zcl_type + 0x1000)
#define MOCK_ATTRIBUTE_CONFIG_NULLABLE(zcl_type)                                                                                   \
    MockAttributeConfig(MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(zcl_type), zcl_type,                                                   \
                        MATTER_ATTRIBUTE_FLAG_WRITABLE | MATTER_ATTRIBUTE_FLAG_READABLE | MATTER_ATTRIBUTE_FLAG_NULLABLE)

#define MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(zcl_type) MockAttributeId(zcl_type + 0x2000)
#define MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(zcl_type)                                                                               \
    MockAttributeConfig(MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(zcl_type), zcl_type,                                               \
                        MATTER_ATTRIBUTE_FLAG_WRITABLE | MATTER_ATTRIBUTE_FLAG_READABLE)

// clang-format off
const MockNodeConfig gTestNodeConfig({
    MockEndpointConfig(kMockEndpoint1, {
        MockClusterConfig(MockClusterId(1), {
            ClusterRevision::Id, FeatureMap::Id,
        }, {
            MockEventId(1), MockEventId(2),
        }),
        MockClusterConfig(MockClusterId(2), {
            ClusterRevision::Id, FeatureMap::Id, MockAttributeId(1)
        }),
        MockClusterConfig(MockClusterId(3), {}, {}, {}, {}, BitMask<MockClusterSide>().Set(MockClusterSide::kClient)),
        MockClusterConfig(MockClusterId(4), {}, {}, {}, {}, BitMask<MockClusterSide>().Set(MockClusterSide::kClient)),
    }, {
        { kDeviceTypeId1, kDeviceTypeId1Version},
        { kDeviceTypeId2, kDeviceTypeId2Version},
        { kDeviceTypeId3, kDeviceTypeId3Version},
    },{
        { MakeNullable(VendorId::TestVendor1), kNamespaceID1, kTag1, MakeOptional(MakeNullable(CharSpan::fromCharString(kLabel1)))},
        { Nullable<VendorId>(), kNamespaceID2, kTag2, MakeOptional(MakeNullable(CharSpan::fromCharString(kLabel2)))},
        { MakeNullable(VendorId::TestVendor3), kNamespaceID3, kTag3, NullOptional},
    }),
    MockEndpointConfig(kMockEndpoint2, {
        MockClusterConfig(MockClusterId(1), {
            ClusterRevision::Id, FeatureMap::Id,
        }),
        MockClusterConfig(
            MockClusterId(2),
            {
               ClusterRevision::Id,
               FeatureMap::Id,
               MockAttributeId(1),
               MockAttributeConfig(MockAttributeId(2), ZCL_ARRAY_ATTRIBUTE_TYPE),
            },          /* attributes */
            {},         /* events */
            {1, 2, 23}, /* acceptedCommands */
            {2, 10}     /* generatedCommands */
        ),
        MockClusterConfig(
            MockClusterId(3),
            {
                ClusterRevision::Id, FeatureMap::Id, MockAttributeId(1), MockAttributeId(2), MockAttributeId(3),
            },    /* attributes */
            {},   /* events */
            {11}, /* acceptedCommands */
            {4, 6},   /* generatedCommands */
            BitMask<MockClusterSide>().Set(MockClusterSide::kClient).Set(MockClusterSide::kServer)
        ),
        MockClusterConfig(MockClusterId(4), {}, {}, {}, {}, MockClusterSide::kClient),
    }, {
        { kDeviceTypeId2, kDeviceTypeId2Version},
    }, {},
    EndpointComposition::kTree),
    MockEndpointConfig(kMockEndpoint3, {
        MockClusterConfig(MockClusterId(1), {
            ClusterRevision::Id, FeatureMap::Id, MockAttributeId(1),
        }),
        MockClusterConfig(MockClusterId(2), {
            ClusterRevision::Id, FeatureMap::Id, MockAttributeId(1), MockAttributeId(2), MockAttributeId(3), MockAttributeId(4),
        }),
        MockClusterConfig(MockClusterId(3), {
            ClusterRevision::Id, FeatureMap::Id,
            MockAttributeConfig(
              kReadOnlyAttributeId,
              ZCL_INT32U_ATTRIBUTE_TYPE,
              MATTER_ATTRIBUTE_FLAG_READABLE | MATTER_ATTRIBUTE_FLAG_NULLABLE   // NOTE: explicitly NOT MATTER_ATTRIBUTE_FLAG_WRITABLE
            )
        }),
        MockClusterConfig(MockClusterId(4), {
            ClusterRevision::Id,
            FeatureMap::Id,
            // several attributes of varying data types for testing.
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_BOOLEAN_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_BITMAP8_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_BITMAP16_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_BITMAP32_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_BITMAP64_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT8U_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT16U_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT24U_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT32U_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT40U_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT48U_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT56U_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT64U_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT8S_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT16S_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT24S_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT32S_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT40S_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT48S_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT56S_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_INT64S_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_ENUM8_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_ENUM16_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_PRIORITY_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_STATUS_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_SINGLE_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_DOUBLE_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_OCTET_STRING_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_CHAR_STRING_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_ARRAY_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_STRUCT_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_GROUP_ID_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_ENDPOINT_NO_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_VENDOR_ID_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_DEVTYPE_ID_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_FABRIC_ID_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_FABRIC_IDX_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_ENTRY_IDX_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_DATA_VER_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_EVENT_NO_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_SEMTAG_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_NAMESPACE_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_TAG_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_SYSTIME_US_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_SYSTIME_MS_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_ELAPSED_S_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_TEMPERATURE_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_TOD_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_DATE_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_EPOCH_US_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_EPOCH_S_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_POSIX_MS_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_PERCENT_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_PERCENT100THS_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_CLUSTER_ID_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_ATTRIB_ID_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_FIELD_ID_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_EVENT_ID_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_COMMAND_ID_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_ACTION_ID_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_TRANS_ID_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_NODE_ID_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_IPADR_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_IPV4ADR_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_IPV6ADR_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_IPV6PRE_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NON_NULLABLE(ZCL_HWADR_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_BOOLEAN_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_BITMAP8_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_BITMAP16_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_BITMAP32_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_BITMAP64_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT8U_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT16U_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT24U_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT32U_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT40U_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT48U_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT56U_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT64U_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT8S_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT16S_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT24S_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT32S_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT40S_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT48S_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT56S_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_INT64S_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_ENUM8_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_ENUM16_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_PRIORITY_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_STATUS_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_SINGLE_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_DOUBLE_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_OCTET_STRING_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_CHAR_STRING_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_ARRAY_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_STRUCT_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_GROUP_ID_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_ENDPOINT_NO_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_VENDOR_ID_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_DEVTYPE_ID_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_FABRIC_ID_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_FABRIC_IDX_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_ENTRY_IDX_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_DATA_VER_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_EVENT_NO_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_SEMTAG_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_NAMESPACE_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_TAG_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_SYSTIME_US_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_SYSTIME_MS_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_ELAPSED_S_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_TEMPERATURE_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_TOD_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_DATE_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_EPOCH_US_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_EPOCH_S_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_POSIX_MS_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_PERCENT_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_PERCENT100THS_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_CLUSTER_ID_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_ATTRIB_ID_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_FIELD_ID_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_EVENT_ID_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_COMMAND_ID_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_ACTION_ID_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_TRANS_ID_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_NODE_ID_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_IPADR_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_IPV4ADR_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_IPV6ADR_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_IPV6PRE_ATTRIBUTE_TYPE),
            MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_HWADR_ATTRIBUTE_TYPE),

            // Special case handling
            MockAttributeConfig(kAttributeIdReadOnly, ZCL_INT32S_ATTRIBUTE_TYPE, 0),
            MockAttributeConfig(kAttributeIdTimedWrite, ZCL_INT32S_ATTRIBUTE_TYPE, MATTER_ATTRIBUTE_FLAG_WRITABLE | MATTER_ATTRIBUTE_FLAG_READABLE | MATTER_ATTRIBUTE_FLAG_MUST_USE_TIMED_WRITE ),
            MockAttributeConfig(kAttributeIdFakeAllowsWrite, ZCL_INT32U_ATTRIBUTE_TYPE, 0),
        }),
    }),
    MockEndpointConfig(kMockEndpoint4, {
        MockClusterConfig(MockClusterId(4), {
            ClusterRevision::Id, FeatureMap::Id, MockAttributeId(4),
            MockAttributeConfig(Clusters::Descriptor::Attributes::EndpointUniqueID::Id, ZCL_CHAR_STRING_ATTRIBUTE_TYPE),
        }),
    }, {
        { kDeviceTypeId4, kDeviceTypeId4Version },
    },
    {}, // Empty semantic tags
    EndpointComposition::kTree,
    chip::CharSpan("AABBCCDDEEFFGGHHIIJJKKLLMMNNOO01", strlen("AABBCCDDEEFFGGHHIIJJKKLLMMNNOO01")) // Add endpointUniqueID
    ),
});
// clang-format on

struct UseMockNodeConfig
{
    UseMockNodeConfig(const MockNodeConfig & config) { SetMockNodeConfig(config); }
    ~UseMockNodeConfig() { ResetMockNodeConfig(); }
};

template <typename T>
CHIP_ERROR DecodeList(TLV::TLVReader & reader, std::vector<T> & out)
{
    TLV::TLVType outer;
    ReturnErrorOnFailure(reader.EnterContainer(outer));
    while (true)
    {
        CHIP_ERROR err = reader.Next();

        if (err == CHIP_END_OF_TLV)
        {
            return CHIP_NO_ERROR;
        }
        ReturnErrorOnFailure(err);

        T value;
        ReturnErrorOnFailure(chip::app::DataModel::Decode(reader, value));
        out.emplace_back(std::move(value));
    }
}

class UnsupportedReadAccessInterface : public AttributeAccessInterface
{
public:
    UnsupportedReadAccessInterface(ConcreteAttributePath path) :
        AttributeAccessInterface(MakeOptional(path.mEndpointId), path.mClusterId), mPath(path)
    {}
    ~UnsupportedReadAccessInterface() = default;

    CHIP_ERROR Read(const ConcreteReadAttributePath & path, AttributeValueEncoder &) override
    {
        if (static_cast<const ConcreteAttributePath &>(path) != mPath)
        {
            // returning without trying to handle means "I do not handle this"
            return CHIP_NO_ERROR;
        }

        return CHIP_IM_GLOBAL_STATUS(UnsupportedRead);
    }

    CHIP_ERROR Write(const ConcreteDataAttributePath & path, AttributeValueDecoder &) override
    {
        if (static_cast<const ConcreteAttributePath &>(path) != mPath)
        {
            // returning without trying to handle means "I do not handle this"
            return CHIP_NO_ERROR;
        }

        return CHIP_IM_GLOBAL_STATUS(UnsupportedWrite);
    }

private:
    ConcreteAttributePath mPath;
};

class StructAttributeAccessInterface : public AttributeAccessInterface
{
public:
    StructAttributeAccessInterface(ConcreteAttributePath path) :
        AttributeAccessInterface(MakeOptional(path.mEndpointId), path.mClusterId), mPath(path)
    {}
    ~StructAttributeAccessInterface() = default;

    CHIP_ERROR Read(const ConcreteReadAttributePath & path, AttributeValueEncoder & encoder) override
    {
        if (static_cast<const ConcreteAttributePath &>(path) != mPath)
        {
            // returning without trying to handle means "I do not handle this"
            return CHIP_NO_ERROR;
        }

        return encoder.Encode(mData);
    }

    CHIP_ERROR Write(const ConcreteDataAttributePath & path, AttributeValueDecoder & decoder) override
    {
        if (static_cast<const ConcreteAttributePath &>(path) != mPath)
        {
            // returning without trying to handle means "I do not handle this"
            return CHIP_NO_ERROR;
        }

        return decoder.Decode(mData);
    }

    void SetReturnedData(const Clusters::UnitTesting::Structs::SimpleStruct::Type & data) { mData = data; }
    const Clusters::UnitTesting::Structs::SimpleStruct::Type & GetData() const { return mData; }

private:
    ConcreteAttributePath mPath;
    Clusters::UnitTesting::Structs::SimpleStruct::Type mData;
};

class ErrorAccessInterface : public AttributeAccessInterface
{
public:
    ErrorAccessInterface(ConcreteAttributePath path, CHIP_ERROR err) :
        AttributeAccessInterface(MakeOptional(path.mEndpointId), path.mClusterId), mPath(path), mError(err)
    {}
    ~ErrorAccessInterface() = default;

    CHIP_ERROR Read(const ConcreteReadAttributePath & path, AttributeValueEncoder & encoder) override
    {
        if (static_cast<const ConcreteAttributePath &>(path) != mPath)
        {
            // returning without trying to handle means "I do not handle this"
            return CHIP_NO_ERROR;
        }
        return mError;
    }

    CHIP_ERROR Write(const ConcreteDataAttributePath & path, AttributeValueDecoder & decoder) override
    {
        if (static_cast<const ConcreteAttributePath &>(path) != mPath)
        {
            // returning without trying to handle means "I do not handle this"
            return CHIP_NO_ERROR;
        }
        return mError;
    }

private:
    ConcreteAttributePath mPath;
    CHIP_ERROR mError;
};

class ListAttributeAcessInterface : public AttributeAccessInterface
{
public:
    ListAttributeAcessInterface(ConcreteAttributePath path) :
        AttributeAccessInterface(MakeOptional(path.mEndpointId), path.mClusterId), mPath(path)
    {}
    ~ListAttributeAcessInterface() = default;

    CHIP_ERROR Read(const ConcreteReadAttributePath & path, AttributeValueEncoder & encoder) override
    {
        if (static_cast<const ConcreteAttributePath &>(path) != mPath)
        {
            // returning without trying to handle means "I do not handle this"
            return CHIP_NO_ERROR;
        }

        return encoder.EncodeList([this](const auto & listEncoder) {
            for (unsigned i = 0; i < mCount; i++)
            {
                mData.a = static_cast<uint8_t>(i % 0xFF);
                ReturnErrorOnFailure(listEncoder.Encode(mData));
            }
            return CHIP_NO_ERROR;
        });
    }

    void SetReturnedData(const Clusters::UnitTesting::Structs::SimpleStruct::Type & data) { mData = data; }
    void SetReturnedDataCount(unsigned count) { mCount = count; }

private:
    ConcreteAttributePath mPath;
    Clusters::UnitTesting::Structs::SimpleStruct::Type mData;
    unsigned mCount = 0;
};

/// RAII registration of an attribute access interface
template <typename T>
class RegisteredAttributeAccessInterface
{
public:
    template <typename... Args>
    RegisteredAttributeAccessInterface(Args &&... args) : mData(std::forward<Args>(args)...)
    {
        VerifyOrDie(AttributeAccessInterfaceRegistry::Instance().Register(&mData));
    }
    ~RegisteredAttributeAccessInterface() { AttributeAccessInterfaceRegistry::Instance().Unregister(&mData); }

    T * operator->() { return &mData; }
    T & operator*() { return mData; }

private:
    T mData;
};

class FakeDefaultServerCluster : public DefaultServerCluster
{
public:
    static constexpr uint32_t kFakeFeatureMap      = 0x35;
    static constexpr uint32_t kFakeClusterRevision = 1234;

    static constexpr CommandId kGeneratedCommands[]           = { 1, 2, 3, 100, 200 };
    static constexpr AcceptedCommandEntry kAcceptedCommands[] = {
        { 101 },
        { 102 },
    };

    static constexpr AttributeEntry kExtraAttributes[] = {
        AttributeEntry{ kAttributeIdReadOnly, BitFlags<AttributeQualityFlags>{}, Access::Privilege::kView, std::nullopt },
        AttributeEntry{ kAttributeIdTimedWrite, BitFlags<AttributeQualityFlags>{}, Access::Privilege::kView,
                        Access::Privilege::kOperate },
        AttributeEntry{ kAttributeIdFakeAllowsWrite, BitFlags<AttributeQualityFlags>{}, Access::Privilege::kView,
                        Access::Privilege::kOperate },
    };

    constexpr FakeDefaultServerCluster(ConcreteClusterPath path) : DefaultServerCluster(std::move(path)) {}

    [[nodiscard]] BitFlags<DataModel::ClusterQualityFlags> GetClusterFlags(const ConcreteClusterPath &) const override
    {
        return DataModel::ClusterQualityFlags::kDiagnosticsData;
    }

    CHIP_ERROR AcceptedCommands(const ConcreteClusterPath & path,
                                ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> & builder) override
    {
        return builder.ReferenceExisting(Span<const AcceptedCommandEntry>(kAcceptedCommands));
    }

    CHIP_ERROR GeneratedCommands(const ConcreteClusterPath & path, ReadOnlyBufferBuilder<CommandId> & builder) override
    {
        return builder.ReferenceExisting(Span<const CommandId>(kGeneratedCommands));
    }

    std::optional<DataModel::ActionReturnStatus> InvokeCommand(const DataModel::InvokeRequest & request,
                                                               chip::TLV::TLVReader & input_arguments,
                                                               CommandHandler * handler) override
    {
        return CHIP_ERROR_INCORRECT_STATE;
    }

    DataModel::ActionReturnStatus WriteAttribute(const DataModel::WriteAttributeRequest & request,
                                                 AttributeValueDecoder & decoder) override
    {
        if (request.path.mAttributeId == kAttributeIdFakeAllowsWrite)
        {
            uint32_t value;
            return decoder.Decode(value);
        }
        return CHIP_ERROR_INCORRECT_STATE;
    }

    CHIP_ERROR Attributes(const ConcreteClusterPath & path, ReadOnlyBufferBuilder<DataModel::AttributeEntry> & builder) override
    {
        ReturnErrorOnFailure(builder.ReferenceExisting(kExtraAttributes));
        return DefaultServerCluster::Attributes(path, builder);
    }

    DataModel::ActionReturnStatus ReadAttribute(const DataModel::ReadAttributeRequest & request,
                                                AttributeValueEncoder & encoder) override
    {
        using namespace chip::app::Clusters;

        switch (request.path.mAttributeId)
        {
        case Globals::Attributes::FeatureMap::Id: {
            uint32_t value = kFakeFeatureMap;
            return encoder.Encode<uint32_t>(std::move(value));
        }
        case Globals::Attributes::ClusterRevision::Id: {
            uint32_t value = kFakeClusterRevision;
            return encoder.Encode<uint32_t>(std::move(value));
        }
        case kAttributeIdFakeAllowsWrite: {
            uint32_t value = 0;
            return encoder.Encode<uint32_t>(std::move(value));
        }
        }
        return CHIP_ERROR_INVALID_ARGUMENT;
    }

    void TestIncreaseDataVersion() { IncreaseDataVersion(); }
    void TestNotifyAttributeChanged(AttributeId attributeId) { NotifyAttributeChanged(attributeId); }

    CHIP_ERROR EventInfo(const ConcreteEventPath & path, DataModel::EventEntry & eventInfo) override
    {
        eventInfo.readPrivilege = mEventInfoFakePrivilege;
        return CHIP_NO_ERROR;
    }

    Access::Privilege mEventInfoFakePrivilege = Access::Privilege::kView;
};

template <typename T, EmberAfAttributeType ZclType>
void TestEmberScalarTypeRead(typename NumericAttributeTraits<T>::WorkingType value)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    ReadOperation testRequest(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZclType));
    testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);

    // Ember encoding for integers is IDENTICAL to the in-memory representation for them
    typename NumericAttributeTraits<T>::StorageType storage;
    NumericAttributeTraits<T>::WorkingToStorage(value, storage);
    chip::Testing::SetEmberReadOutput(ByteSpan(reinterpret_cast<const uint8_t *>(&storage), sizeof(storage)));

    // Data read via the encoder
    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
    ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), CHIP_NO_ERROR);
    ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR);

    // Validate after read
    std::vector<DecodedAttributeData> attribute_data;
    ASSERT_EQ(testRequest.GetEncodedIBs().Decode(attribute_data), CHIP_NO_ERROR);
    ASSERT_EQ(attribute_data.size(), 1u);

    DecodedAttributeData & encodedData = attribute_data[0];
    ASSERT_EQ(encodedData.attributePath, testRequest.GetRequest().path);

    typename NumericAttributeTraits<T>::WorkingType actual;
    ASSERT_EQ(chip::app::DataModel::Decode<typename NumericAttributeTraits<T>::WorkingType>(encodedData.dataReader, actual),
              CHIP_NO_ERROR);
    ASSERT_EQ(actual, value);
}

template <typename T, EmberAfAttributeType ZclType>
void TestEmberScalarNullRead()
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    ReadOperation testRequest(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZclType));
    testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);

    // Ember encoding for integers is IDENTICAL to the in-memory representation for them
    typename NumericAttributeTraits<T>::StorageType nullValue;
    NumericAttributeTraits<T>::SetNull(nullValue);
    chip::Testing::SetEmberReadOutput(ByteSpan(reinterpret_cast<const uint8_t *>(&nullValue), sizeof(nullValue)));

    // Data read via the encoder
    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
    ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), CHIP_NO_ERROR);
    ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR);

    // Validate after read
    std::vector<DecodedAttributeData> attribute_data;
    ASSERT_EQ(testRequest.GetEncodedIBs().Decode(attribute_data), CHIP_NO_ERROR);
    ASSERT_EQ(attribute_data.size(), 1u);

    DecodedAttributeData & encodedData = attribute_data[0];
    ASSERT_EQ(encodedData.attributePath, testRequest.GetRequest().path);
    chip::app::DataModel::Nullable<typename NumericAttributeTraits<T>::WorkingType> actual;
    ASSERT_EQ(chip::app::DataModel::Decode(encodedData.dataReader, actual), CHIP_NO_ERROR);
    ASSERT_TRUE(actual.IsNull());
}

template <typename T, EmberAfAttributeType ZclType>
void TestEmberScalarTypeWrite(const typename NumericAttributeTraits<T>::WorkingType value)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    // non-nullable test
    {
        WriteOperation test(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZclType));
        test.SetSubjectDescriptor(kAdminSubjectDescriptor);

        AttributeValueDecoder decoder = test.DecoderFor(value);

        // write should succeed
        ASSERT_TRUE(model.WriteAttribute(test.GetRequest(), decoder).IsSuccess());

        // Validate data after write
        chip::ByteSpan writtenData = chip::Testing::GetEmberBuffer();

        typename NumericAttributeTraits<T>::StorageType storage;
        ASSERT_GE(writtenData.size(), sizeof(storage));
        memcpy(&storage, writtenData.data(), sizeof(storage));
        typename NumericAttributeTraits<T>::WorkingType actual = NumericAttributeTraits<T>::StorageToWorking(storage);

        EXPECT_EQ(actual, value);
        ASSERT_EQ(model.ChangeListener().DirtyList().size(), 1u);
        EXPECT_EQ(model.ChangeListener().DirtyList()[0],
                  AttributePathParams(test.GetRequest().path.mEndpointId, test.GetRequest().path.mClusterId,
                                      test.GetRequest().path.mAttributeId));

        // reset for the next test
        model.ChangeListener().DirtyList().clear();
    }

    // nullable test: write null to make sure content of buffer changed (otherwise it will be a noop for dirty checking)
    {
        WriteOperation test(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZclType));
        test.SetSubjectDescriptor(kAdminSubjectDescriptor);

        using NumericType             = NumericAttributeTraits<T>;
        using NullableType            = chip::app::DataModel::Nullable<typename NumericType::WorkingType>;
        AttributeValueDecoder decoder = test.DecoderFor<NullableType>(NullableType());

        // write should succeed
        ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_NO_ERROR);

        // dirty: we changed the value to null
        ASSERT_EQ(model.ChangeListener().DirtyList().size(), 1u);
        EXPECT_EQ(model.ChangeListener().DirtyList()[0],
                  AttributePathParams(test.GetRequest().path.mEndpointId, test.GetRequest().path.mClusterId,
                                      test.GetRequest().path.mAttributeId));
    }

    // nullable test
    {
        WriteOperation test(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZclType));
        test.SetSubjectDescriptor(kAdminSubjectDescriptor);

        AttributeValueDecoder decoder = test.DecoderFor(value);

        // write should succeed
        ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_NO_ERROR);

        // Validate data after write
        chip::ByteSpan writtenData = chip::Testing::GetEmberBuffer();

        typename NumericAttributeTraits<T>::StorageType storage;
        ASSERT_GE(writtenData.size(), sizeof(storage));
        memcpy(&storage, writtenData.data(), sizeof(storage));
        typename NumericAttributeTraits<T>::WorkingType actual = NumericAttributeTraits<T>::StorageToWorking(storage);

        ASSERT_EQ(actual, value);
        // dirty a 2nd time when we moved from null to a real value
        ASSERT_EQ(model.ChangeListener().DirtyList().size(), 2u);
        EXPECT_EQ(model.ChangeListener().DirtyList()[1],
                  AttributePathParams(test.GetRequest().path.mEndpointId, test.GetRequest().path.mClusterId,
                                      test.GetRequest().path.mAttributeId));
    }
}

template <typename T, EmberAfAttributeType ZclType>
void TestEmberScalarNullWrite()
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    WriteOperation test(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZclType));
    test.SetSubjectDescriptor(kAdminSubjectDescriptor);

    using NumericType             = NumericAttributeTraits<T>;
    using NullableType            = chip::app::DataModel::Nullable<typename NumericType::WorkingType>;
    AttributeValueDecoder decoder = test.DecoderFor<NullableType>(NullableType());

    // write should succeed
    ASSERT_TRUE(model.WriteAttribute(test.GetRequest(), decoder).IsSuccess());

    // Validate data after write
    chip::ByteSpan writtenData = chip::Testing::GetEmberBuffer();

    using Traits = NumericAttributeTraits<T>;

    typename Traits::StorageType storage;
    ASSERT_GE(writtenData.size(), sizeof(storage));
    memcpy(&storage, writtenData.data(), sizeof(storage));
    ASSERT_TRUE(Traits::IsNullValue(storage));
}

template <typename T, EmberAfAttributeType ZclType>
void TestEmberScalarTypeWriteNullValueToNullable()
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    WriteOperation test(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZclType));
    test.SetSubjectDescriptor(kAdminSubjectDescriptor);

    using NumericType             = NumericAttributeTraits<T>;
    using NullableType            = chip::app::DataModel::Nullable<typename NumericType::WorkingType>;
    AttributeValueDecoder decoder = test.DecoderFor<NullableType>(NullableType());

    // write should fail: we are trying to write null
    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_ERROR_WRONG_TLV_TYPE);
}

} // namespace

uint16_t ReadLe16(const void * buffer)
{
    const uint8_t * p = reinterpret_cast<const uint8_t *>(buffer);
    return chip::Encoding::LittleEndian::Read16(p);
}

void WriteLe16(void * buffer, uint16_t value)
{
    uint8_t * p = reinterpret_cast<uint8_t *>(buffer);
    chip::Encoding::LittleEndian::Write16(p, value);
}

TEST_F(TestCodegenModelViaMocks, IterateOverEndpoints)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;

    // This iteration relies on the hard-coding that occurs when mock_ember is used
    ReadOnlyBufferBuilder<DataModel::EndpointEntry> endpointsBuilder;

    ASSERT_EQ(model.Endpoints(endpointsBuilder), CHIP_NO_ERROR);

    auto endpoints = endpointsBuilder.TakeBuffer();

    ASSERT_EQ(endpoints.size(), 4u);

    EXPECT_EQ(endpoints[0].id, kMockEndpoint1);
    EXPECT_EQ(endpoints[0].parentId, kInvalidEndpointId);
    EXPECT_EQ(endpoints[0].compositionPattern, EndpointCompositionPattern::kFullFamily);

    EXPECT_EQ(endpoints[1].id, kMockEndpoint2);
    EXPECT_EQ(endpoints[1].parentId, kInvalidEndpointId);
    EXPECT_EQ(endpoints[1].compositionPattern, EndpointCompositionPattern::kTree);

    EXPECT_EQ(endpoints[2].id, kMockEndpoint3);
    EXPECT_EQ(endpoints[2].parentId, kInvalidEndpointId);
    EXPECT_EQ(endpoints[2].compositionPattern, EndpointCompositionPattern::kFullFamily);
}

TEST_F(TestCodegenModelViaMocks, IterateOverServerClusters)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;

    chip::Testing::ResetVersion();

    ReadOnlyBufferBuilder<DataModel::ServerClusterEntry> builder;

    EXPECT_NE(model.ServerClusters(kEndpointIdThatIsMissing, builder), CHIP_NO_ERROR);
    EXPECT_TRUE(builder.IsEmpty());
    EXPECT_NE(model.ServerClusters(kInvalidEndpointId, builder), CHIP_NO_ERROR);
    EXPECT_TRUE(builder.IsEmpty());

    // mock endpoint 1 has 2 mock clusters: 1 and 2
    EXPECT_EQ(model.ServerClusters(kMockEndpoint1, builder), CHIP_NO_ERROR);
    auto serverClusters = builder.TakeBuffer();
    ASSERT_EQ(serverClusters.size(), 2u);

    EXPECT_EQ(serverClusters[0].clusterId, MockClusterId(1));
    EXPECT_EQ(serverClusters[0].dataVersion, 0u);
    EXPECT_EQ(serverClusters[0].flags.Raw(), 0u);

    EXPECT_EQ(serverClusters[1].clusterId, MockClusterId(2));
    EXPECT_EQ(serverClusters[1].dataVersion, 0u);
    EXPECT_EQ(serverClusters[1].flags.Raw(), 0u);

    chip::Testing::BumpVersion();

    EXPECT_EQ(model.ServerClusters(kMockEndpoint1, builder), CHIP_NO_ERROR);
    serverClusters = builder.TakeBuffer();
    ASSERT_EQ(serverClusters.size(), 2u);
    EXPECT_EQ(serverClusters[0].dataVersion, 1u);
    EXPECT_EQ(serverClusters[1].dataVersion, 1u);

    // mock endpoint 3 has 4 mock clusters: 1 through 4
    EXPECT_EQ(model.ServerClusters(kMockEndpoint3, builder), CHIP_NO_ERROR);
    serverClusters = builder.TakeBuffer();
    ASSERT_EQ(serverClusters.size(), 4u);
    EXPECT_EQ(serverClusters[0].clusterId, MockClusterId(1));
    EXPECT_EQ(serverClusters[1].clusterId, MockClusterId(2));
    EXPECT_EQ(serverClusters[2].clusterId, MockClusterId(3));
    EXPECT_EQ(serverClusters[3].clusterId, MockClusterId(4));
}

TEST_F(TestCodegenModelViaMocks, IterateOverClientClusters)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;

    ReadOnlyBufferBuilder<ClusterId> builder;

    EXPECT_EQ(model.ClientClusters(kEndpointIdThatIsMissing, builder), CHIP_ERROR_NOT_FOUND);
    EXPECT_TRUE(builder.IsEmpty());
    EXPECT_EQ(model.ClientClusters(kInvalidEndpointId, builder), CHIP_ERROR_NOT_FOUND);
    EXPECT_TRUE(builder.IsEmpty());

    // mock endpoint 1 has 2 mock client clusters: 3 and 4
    EXPECT_EQ(model.ClientClusters(kMockEndpoint1, builder), CHIP_NO_ERROR);
    auto clientClusters = builder.TakeBuffer();

    const ClusterId kExpectedClusters1[] = { MockClusterId(3), MockClusterId(4) };
    ASSERT_TRUE(clientClusters.data_equal(Span<const ClusterId>(kExpectedClusters1)));

    // mock endpoint 2 has 1 mock client clusters: 3(has server side at the same time) and 4
    EXPECT_EQ(model.ClientClusters(kMockEndpoint2, builder), CHIP_NO_ERROR);
    clientClusters = builder.TakeBuffer();

    const ClusterId kExpectedClusters2[] = { MockClusterId(3), MockClusterId(4) };
    ASSERT_TRUE(clientClusters.data_equal(Span<const ClusterId>(kExpectedClusters2)));
}

TEST_F(TestCodegenModelViaMocks, IterateOverAttributes)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;

    // invalid paths should return in "no more data"
    ASSERT_TRUE(model.AttributesIgnoreError(ConcreteClusterPath(kEndpointIdThatIsMissing, MockClusterId(1))).empty());
    ASSERT_TRUE(model.AttributesIgnoreError(ConcreteClusterPath(kInvalidEndpointId, MockClusterId(1))).empty());
    ASSERT_TRUE(model.AttributesIgnoreError(ConcreteClusterPath(kMockEndpoint1, MockClusterId(10))).empty());
    ASSERT_TRUE(model.AttributesIgnoreError(ConcreteClusterPath(kMockEndpoint1, kInvalidClusterId)).empty());

    // should be able to iterate over valid paths
    ReadOnlyBufferBuilder<DataModel::AttributeEntry> builder;

    // invalid paths return errors
    ASSERT_EQ(model.Attributes(ConcreteClusterPath(kEndpointIdThatIsMissing, MockClusterId(1)), builder), CHIP_ERROR_NOT_FOUND);
    ASSERT_EQ(model.Attributes(ConcreteClusterPath(kInvalidEndpointId, MockClusterId(1)), builder), CHIP_ERROR_NOT_FOUND);
    ASSERT_EQ(model.Attributes(ConcreteClusterPath(kMockEndpoint1, MockClusterId(10)), builder), CHIP_ERROR_NOT_FOUND);
    ASSERT_EQ(model.Attributes(ConcreteClusterPath(kMockEndpoint1, kInvalidClusterId), builder), CHIP_ERROR_NOT_FOUND);

    EXPECT_EQ(model.Attributes(ConcreteClusterPath(kMockEndpoint2, MockClusterId(2)), builder), CHIP_NO_ERROR);
    auto attributes = builder.TakeBuffer();
    ASSERT_EQ(attributes.size(), 7u);

    ASSERT_EQ(attributes[0].attributeId, ClusterRevision::Id);
    ASSERT_FALSE(attributes[0].HasFlags(AttributeQualityFlags::kListAttribute));

    ASSERT_EQ(attributes[1].attributeId, FeatureMap::Id);
    ASSERT_FALSE(attributes[1].HasFlags(AttributeQualityFlags::kListAttribute));

    ASSERT_EQ(attributes[2].attributeId, MockAttributeId(1));
    ASSERT_FALSE(attributes[2].HasFlags(AttributeQualityFlags::kListAttribute));

    ASSERT_EQ(attributes[3].attributeId, MockAttributeId(2));
    ASSERT_TRUE(attributes[3].HasFlags(AttributeQualityFlags::kListAttribute));

    // Ends with global list attributes
    ASSERT_EQ(attributes[4].attributeId, GeneratedCommandList::Id);
    ASSERT_TRUE(attributes[4].HasFlags(AttributeQualityFlags::kListAttribute));

    ASSERT_EQ(attributes[5].attributeId, AcceptedCommandList::Id);
    ASSERT_TRUE(attributes[5].HasFlags(AttributeQualityFlags::kListAttribute));

    ASSERT_EQ(attributes[6].attributeId, AttributeList::Id);
    ASSERT_TRUE(attributes[6].HasFlags(AttributeQualityFlags::kListAttribute));
}

TEST_F(TestCodegenModelViaMocks, FindAttribute)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;

    AttributeFinder finder(&model);

    // various non-existent or invalid paths should return no info data
    ASSERT_FALSE(finder.Find(ConcreteAttributePath(kInvalidEndpointId, kInvalidClusterId, kInvalidAttributeId)).has_value());
    ASSERT_FALSE(finder.Find(ConcreteAttributePath(kInvalidEndpointId, kInvalidClusterId, FeatureMap::Id)).has_value());
    ASSERT_FALSE(finder.Find(ConcreteAttributePath(kInvalidEndpointId, MockClusterId(1), FeatureMap::Id)).has_value());
    ASSERT_FALSE(finder.Find(ConcreteAttributePath(kMockEndpoint1, kInvalidClusterId, FeatureMap::Id)).has_value());
    ASSERT_FALSE(finder.Find(ConcreteAttributePath(kMockEndpoint1, MockClusterId(10), FeatureMap::Id)).has_value());
    ASSERT_FALSE(finder.Find(ConcreteAttributePath(kMockEndpoint1, MockClusterId(10), kInvalidAttributeId)).has_value());
    ASSERT_FALSE(finder.Find(ConcreteAttributePath(kMockEndpoint1, MockClusterId(1), MockAttributeId(10))).has_value());

    // valid info
    std::optional<AttributeEntry> info1 = finder.Find(ConcreteAttributePath(kMockEndpoint1, MockClusterId(1), FeatureMap::Id));
    ASSERT_TRUE(info1.has_value());
    EXPECT_FALSE(info1->HasFlags(AttributeQualityFlags::kListAttribute)); // NOLINT(bugprone-unchecked-optional-access)

    // Mocks always set everything as R/W with administrative privileges
    EXPECT_EQ(info1->GetReadPrivilege(), chip::Access::Privilege::kAdminister);  // NOLINT(bugprone-unchecked-optional-access)
    EXPECT_EQ(info1->GetWritePrivilege(), chip::Access::Privilege::kAdminister); // NOLINT(bugprone-unchecked-optional-access)

    std::optional<AttributeEntry> info2 = finder.Find(ConcreteAttributePath(kMockEndpoint2, MockClusterId(2), MockAttributeId(2)));
    ASSERT_TRUE(info2.has_value());
    EXPECT_TRUE(info2->HasFlags(AttributeQualityFlags::kListAttribute));         // NOLINT(bugprone-unchecked-optional-access)
    EXPECT_EQ(info2->GetReadPrivilege(), chip::Access::Privilege::kAdminister);  // NOLINT(bugprone-unchecked-optional-access)
    EXPECT_EQ(info2->GetWritePrivilege(), chip::Access::Privilege::kAdminister); // NOLINT(bugprone-unchecked-optional-access)

    // test a read-only attribute, which will not have a write privilege
    std::optional<AttributeEntry> info3 =
        finder.Find(ConcreteAttributePath(kMockEndpoint3, MockClusterId(3), kReadOnlyAttributeId));
    ASSERT_TRUE(info3.has_value());
    EXPECT_FALSE(info3->HasFlags(AttributeQualityFlags::kListAttribute));       // NOLINT(bugprone-unchecked-optional-access)
    EXPECT_EQ(info3->GetReadPrivilege(), chip::Access::Privilege::kAdminister); // NOLINT(bugprone-unchecked-optional-access)
    EXPECT_FALSE(info3->GetWritePrivilege().has_value());                       // NOLINT(bugprone-unchecked-optional-access)
}

// global attributes are EXPLICITLY supported
TEST_F(TestCodegenModelViaMocks, GlobalAttributeInfo)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;

    AttributeFinder finder(&model);

    std::optional<AttributeEntry> info1 = finder.Find(
        ConcreteAttributePath(kMockEndpoint1, MockClusterId(1), Clusters::Globals::Attributes::GeneratedCommandList::Id));

    ASSERT_TRUE(info1.has_value());

    std::optional<AttributeEntry> info2 =
        finder.Find(ConcreteAttributePath(kMockEndpoint1, MockClusterId(1), Clusters::Globals::Attributes::AttributeList::Id));
    ASSERT_TRUE(info2.has_value());

    std::optional<AttributeEntry> info3 = finder.Find(
        ConcreteAttributePath(kMockEndpoint1, MockClusterId(1), Clusters::Globals::Attributes::AcceptedCommandList::Id));
    ASSERT_TRUE(info3.has_value());
}

TEST_F(TestCodegenModelViaMocks, IterateOverAcceptedCommands)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;

    ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> builder;

    // invalid paths should return in "no more data"
    ASSERT_EQ(model.AcceptedCommands(ConcreteClusterPath(kEndpointIdThatIsMissing, MockClusterId(1)), builder),
              CHIP_ERROR_NOT_FOUND);
    ASSERT_TRUE(builder.IsEmpty());
    ASSERT_EQ(model.AcceptedCommands(ConcreteClusterPath(kInvalidEndpointId, MockClusterId(1)), builder), CHIP_ERROR_NOT_FOUND);
    ASSERT_TRUE(builder.IsEmpty());
    ASSERT_EQ(model.AcceptedCommands(ConcreteClusterPath(kMockEndpoint1, MockClusterId(10)), builder), CHIP_ERROR_NOT_FOUND);
    ASSERT_TRUE(builder.IsEmpty());
    ASSERT_EQ(model.AcceptedCommands(ConcreteClusterPath(kMockEndpoint1, kInvalidClusterId), builder), CHIP_ERROR_NOT_FOUND);
    ASSERT_TRUE(builder.IsEmpty());

    ASSERT_EQ(model.AcceptedCommands(ConcreteClusterPath(kMockEndpoint2, MockClusterId(2)), builder), CHIP_NO_ERROR);
    ASSERT_EQ(builder.Size(), 3u);

    auto cmds = builder.TakeBuffer();

    // took ownership
    ASSERT_EQ(builder.Size(), 0u);
    ASSERT_TRUE(builder.IsEmpty());

    ASSERT_EQ(cmds.size(), 3u);
    ASSERT_EQ(cmds[0].commandId, 1u);
    ASSERT_EQ(cmds[1].commandId, 2u);
    ASSERT_EQ(cmds[2].commandId, 23u);
}

TEST_F(TestCodegenModelViaMocks, IterateOverGeneratedCommands)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;

    ReadOnlyBufferBuilder<CommandId> builder;

    // invalid paths should return in "no more data"
    ASSERT_EQ(model.GeneratedCommands(ConcreteClusterPath(kEndpointIdThatIsMissing, MockClusterId(1)), builder),
              CHIP_ERROR_NOT_FOUND);
    ASSERT_TRUE(builder.IsEmpty());
    ASSERT_EQ(model.GeneratedCommands(ConcreteClusterPath(kInvalidEndpointId, MockClusterId(1)), builder), CHIP_ERROR_NOT_FOUND);
    ASSERT_TRUE(builder.IsEmpty());
    ASSERT_EQ(model.GeneratedCommands(ConcreteClusterPath(kMockEndpoint1, MockClusterId(10)), builder), CHIP_ERROR_NOT_FOUND);
    ASSERT_TRUE(builder.IsEmpty());
    ASSERT_EQ(model.GeneratedCommands(ConcreteClusterPath(kMockEndpoint1, kInvalidClusterId), builder), CHIP_ERROR_NOT_FOUND);
    ASSERT_TRUE(builder.IsEmpty());

    // should be able to iterate over valid paths
    ASSERT_EQ(model.GeneratedCommands(ConcreteClusterPath(kMockEndpoint2, MockClusterId(2)), builder), CHIP_NO_ERROR);
    auto cmds = builder.TakeBuffer();

    const CommandId expectedCommands2[] = { 2, 10 };
    ASSERT_TRUE(cmds.data_equal(Span<const CommandId>(expectedCommands2)));

    ASSERT_EQ(model.GeneratedCommands(ConcreteClusterPath(kMockEndpoint2, MockClusterId(3)), builder), CHIP_NO_ERROR);
    cmds                                = builder.TakeBuffer();
    const CommandId expectedCommands3[] = { 4, 6 };
    ASSERT_TRUE(cmds.data_equal(Span<const CommandId>(expectedCommands3)));
}

TEST_F(TestCodegenModelViaMocks, AcceptedGeneratedCommandsOnInvalidEndpoints)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;

    // register a CHI on ALL endpoints
    CustomListCommandHandler handler(chip::NullOptional, MockClusterId(1));
    handler.SetHandleCommands(true);

    ReadOnlyBufferBuilder<CommandId> generatedBuilder;
    ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> acceptedBuilder;

    // valid endpoint will result in valid data (even though list is empty)
    ASSERT_EQ(model.GeneratedCommands(ConcreteClusterPath(kMockEndpoint1, MockClusterId(1)), generatedBuilder), CHIP_NO_ERROR);
    ASSERT_TRUE(generatedBuilder.IsEmpty());
    ASSERT_EQ(model.AcceptedCommands(ConcreteClusterPath(kMockEndpoint1, MockClusterId(1)), acceptedBuilder), CHIP_NO_ERROR);
    ASSERT_TRUE(acceptedBuilder.IsEmpty());

    // Invalid endpoint fails - we will get no commands there (even though CHI is registered)
    ASSERT_EQ(model.GeneratedCommands(ConcreteClusterPath(kEndpointIdThatIsMissing, MockClusterId(1)), generatedBuilder),
              CHIP_ERROR_NOT_FOUND);
    ASSERT_EQ(model.AcceptedCommands(ConcreteClusterPath(kEndpointIdThatIsMissing, MockClusterId(1)), acceptedBuilder),
              CHIP_ERROR_NOT_FOUND);

    // same for invalid cluster ID
    ASSERT_EQ(model.GeneratedCommands(ConcreteClusterPath(kMockEndpoint1, MockClusterId(0x1123)), generatedBuilder),
              CHIP_ERROR_NOT_FOUND);
    ASSERT_EQ(model.AcceptedCommands(ConcreteClusterPath(kMockEndpoint1, MockClusterId(0x1123)), acceptedBuilder),
              CHIP_ERROR_NOT_FOUND);
}

TEST_F(TestCodegenModelViaMocks, AcceptedGeneratedCommandsOnInvalidEndpointsUsingShim)
{

    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;

    // register a CHI on ALL endpoints
    ShimCommandHandler handler(chip::NullOptional, MockClusterId(1));
    handler.SetHandleCommands(true);

    ReadOnlyBufferBuilder<CommandId> generatedBuilder;
    ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> acceptedBuilder;

    // valid endpoint will result in valid data (even though list is empty)
    ASSERT_EQ(model.GeneratedCommands(ConcreteClusterPath(kMockEndpoint1, MockClusterId(1)), generatedBuilder), CHIP_NO_ERROR);
    ASSERT_TRUE(generatedBuilder.IsEmpty());
    ASSERT_EQ(model.AcceptedCommands(ConcreteClusterPath(kMockEndpoint1, MockClusterId(1)), acceptedBuilder), CHIP_NO_ERROR);
    ASSERT_TRUE(acceptedBuilder.IsEmpty());

    // Invalid endpoint fails - we will get no commands there (even though CHI is registered)
    ASSERT_EQ(model.GeneratedCommands(ConcreteClusterPath(kEndpointIdThatIsMissing, MockClusterId(1)), generatedBuilder),
              CHIP_ERROR_NOT_FOUND);
    ASSERT_EQ(model.AcceptedCommands(ConcreteClusterPath(kEndpointIdThatIsMissing, MockClusterId(1)), acceptedBuilder),
              CHIP_ERROR_NOT_FOUND);

    // same for invalid cluster ID
    ASSERT_EQ(model.GeneratedCommands(ConcreteClusterPath(kMockEndpoint1, MockClusterId(0x1123)), generatedBuilder),
              CHIP_ERROR_NOT_FOUND);
    ASSERT_EQ(model.AcceptedCommands(ConcreteClusterPath(kMockEndpoint1, MockClusterId(0x1123)), acceptedBuilder),
              CHIP_ERROR_NOT_FOUND);
}

TEST_F(TestCodegenModelViaMocks, CommandHandlerInterfaceCommandHandling)
{

    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;

    // Command handler interface is capable to override accepted and generated commands.
    // Validate that these work
    CustomListCommandHandler handler(MakeOptional(kMockEndpoint1), MockClusterId(1));

    ReadOnlyBufferBuilder<CommandId> generatedBuilder;
    ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> acceptedBuilder;

    // At this point, without overrides, there should be no accepted/generated commands
    ASSERT_EQ(model.GeneratedCommands(ConcreteClusterPath(kMockEndpoint1, MockClusterId(1)), generatedBuilder), CHIP_NO_ERROR);
    ASSERT_TRUE(generatedBuilder.IsEmpty());
    ASSERT_EQ(model.AcceptedCommands(ConcreteClusterPath(kMockEndpoint1, MockClusterId(1)), acceptedBuilder), CHIP_NO_ERROR);
    ASSERT_TRUE(acceptedBuilder.IsEmpty());

    handler.SetOverrideAccepted(true);
    handler.SetOverrideGenerated(true);

    // with overrides, the list is still empty ...
    ASSERT_EQ(model.GeneratedCommands(ConcreteClusterPath(kMockEndpoint1, MockClusterId(1)), generatedBuilder), CHIP_NO_ERROR);
    ASSERT_TRUE(generatedBuilder.IsEmpty());
    ASSERT_EQ(model.AcceptedCommands(ConcreteClusterPath(kMockEndpoint1, MockClusterId(1)), acceptedBuilder), CHIP_NO_ERROR);
    ASSERT_TRUE(acceptedBuilder.IsEmpty());

    // set some overrides
    handler.AcceptedVec().push_back({ 1234 });
    handler.AcceptedVec().push_back({ 999 });

    handler.GeneratedVec().push_back(33);

    ASSERT_EQ(model.AcceptedCommands(ConcreteClusterPath(kMockEndpoint1, MockClusterId(1)), acceptedBuilder), CHIP_NO_ERROR);
    auto acceptedCommands = acceptedBuilder.TakeBuffer();

    ASSERT_EQ(acceptedCommands.size(), 2u);
    ASSERT_EQ(acceptedCommands[0].commandId, 1234u);
    ASSERT_EQ(acceptedCommands[1].commandId, 999u);

    ASSERT_EQ(model.GeneratedCommands(ConcreteClusterPath(kMockEndpoint1, MockClusterId(1)), generatedBuilder), CHIP_NO_ERROR);
    auto generatedCommands = generatedBuilder.TakeBuffer();
    ASSERT_EQ(generatedCommands.size(), std::size_t{ 1 });
    const CommandId expectedGeneratedCommands[] = { 33 };
    ASSERT_TRUE(generatedCommands.data_equal(Span<const CommandId>(expectedGeneratedCommands)));
}

//////
TEST_F(TestCodegenModelViaMocks, ShimCommandHandlerInterfaceCommandHandling)
{
    // This node configuration needs to partially match a cluster tree, using Clusters::Unittesting as a base
    // clang-format off
    using namespace Clusters::UnitTesting;
    static const MockNodeConfig kNodeConfig({
        MockEndpointConfig(kMockEndpoint1, {
            MockClusterConfig(Clusters::UnitTesting::Id, {
                ClusterRevision::Id, FeatureMap::Id,
            }),
            MockClusterConfig(MockClusterId(2), {
                ClusterRevision::Id, FeatureMap::Id, MockAttributeId(1),
            }),
        })
    });
    // clang-format on

    UseMockNodeConfig config(kNodeConfig);

    CodegenDataModelProviderWithContext model;

    // Command handler interface is capable to override accepted and generated commands.
    // Validate that these work
    ShimCommandHandler handler(MakeOptional(kMockEndpoint1), Clusters::UnitTesting::Id);

    ReadOnlyBufferBuilder<CommandId> generatedBuilder;
    ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> acceptedBuilder;

    // At this point, without overrides, there should be no accepted/generated commands
    ASSERT_EQ(model.GeneratedCommands(ConcreteClusterPath(kMockEndpoint1, Clusters::UnitTesting::Id), generatedBuilder),
              CHIP_NO_ERROR);
    ASSERT_TRUE(generatedBuilder.IsEmpty());
    ASSERT_EQ(model.AcceptedCommands(ConcreteClusterPath(kMockEndpoint1, Clusters::UnitTesting::Id), acceptedBuilder),
              CHIP_NO_ERROR);
    ASSERT_TRUE(acceptedBuilder.IsEmpty());

    handler.SetOverrideAccepted(true);
    handler.SetOverrideGenerated(true);

    // with overrides, the list is still empty ...
    ASSERT_EQ(model.GeneratedCommands(ConcreteClusterPath(kMockEndpoint1, Clusters::UnitTesting::Id), generatedBuilder),
              CHIP_NO_ERROR);
    ASSERT_TRUE(generatedBuilder.IsEmpty());
    ASSERT_EQ(model.AcceptedCommands(ConcreteClusterPath(kMockEndpoint1, Clusters::UnitTesting::Id), acceptedBuilder),
              CHIP_NO_ERROR);
    ASSERT_TRUE(acceptedBuilder.IsEmpty());

    // set some overrides
    handler.AcceptedVec().push_back(Commands::Test::Id);
    handler.AcceptedVec().push_back(Commands::TestNotHandled::Id);

    handler.GeneratedVec().push_back(Commands::TestSpecific::Id);

    ASSERT_EQ(model.AcceptedCommands(ConcreteClusterPath(kMockEndpoint1, Clusters::UnitTesting::Id), acceptedBuilder),
              CHIP_NO_ERROR);
    auto acceptedCommands = acceptedBuilder.TakeBuffer();

    ASSERT_EQ(acceptedCommands.size(), 2u);
    ASSERT_EQ(acceptedCommands[0].commandId, Commands::Test::Id);
    ASSERT_EQ(acceptedCommands[1].commandId, Commands::TestNotHandled::Id);

    ASSERT_EQ(model.GeneratedCommands(ConcreteClusterPath(kMockEndpoint1, Clusters::UnitTesting::Id), generatedBuilder),
              CHIP_NO_ERROR);
    auto generatedCommands = generatedBuilder.TakeBuffer();
    ASSERT_EQ(generatedCommands.size(), 1u);

    const CommandId expectedGeneratedCommands[] = { Commands::TestSpecific::Id };
    ASSERT_TRUE(generatedCommands.data_equal(Span<const CommandId>(expectedGeneratedCommands)));
}

TEST_F(TestCodegenModelViaMocks, AccessInterfaceUnsupportedRead)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    const ConcreteAttributePath kTestPath(kMockEndpoint3, MockClusterId(4),
                                          MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_STRUCT_ATTRIBUTE_TYPE));

    ReadOperation testRequest(kTestPath);
    testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);
    testRequest.SetPathExpanded(true);

    RegisteredAttributeAccessInterface<UnsupportedReadAccessInterface> aai(kTestPath);

    // For expanded paths, unsupported read from AAI (i.e. reading write-only data)
    // succeed without attempting to encode.
    // This is temporary until ACL checks are moved inside the IM/ReportEngine
    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
    ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), CHIP_NO_ERROR);
    ASSERT_FALSE(encoder->TriedEncode());
}

TEST_F(TestCodegenModelViaMocks, EmberAttributeReadInt32S)
{
    TestEmberScalarTypeRead<int32_t, ZCL_INT32S_ATTRIBUTE_TYPE>(-1234);
}

TEST_F(TestCodegenModelViaMocks, EmberAttributeReadEnum16)
{
    TestEmberScalarTypeRead<uint16_t, ZCL_ENUM16_ATTRIBUTE_TYPE>(0x1234);
}

TEST_F(TestCodegenModelViaMocks, EmberAttributeReadFloat)
{
    TestEmberScalarTypeRead<float, ZCL_SINGLE_ATTRIBUTE_TYPE>(0.625);
}

TEST_F(TestCodegenModelViaMocks, EmberAttributeReadDouble)
{
    TestEmberScalarTypeRead<double, ZCL_DOUBLE_ATTRIBUTE_TYPE>(0.625);
}

TEST_F(TestCodegenModelViaMocks, EmberAttributeReadInt24U)
{
    TestEmberScalarTypeRead<OddSizedInteger<3, false>, ZCL_INT24U_ATTRIBUTE_TYPE>(0x1234AB);
}

TEST_F(TestCodegenModelViaMocks, EmberAttributeReadInt32U)
{
    TestEmberScalarTypeRead<uint32_t, ZCL_INT32U_ATTRIBUTE_TYPE>(0x1234ABCD);
}

TEST_F(TestCodegenModelViaMocks, EmberAttributeReadInt40U)
{
    TestEmberScalarTypeRead<OddSizedInteger<5, false>, ZCL_INT40U_ATTRIBUTE_TYPE>(0x1122334455);
}

TEST_F(TestCodegenModelViaMocks, EmberAttributeReadInt48U)
{
    TestEmberScalarTypeRead<OddSizedInteger<6, false>, ZCL_INT48U_ATTRIBUTE_TYPE>(0xAABB11223344);
}

TEST_F(TestCodegenModelViaMocks, EmberAttributeReadInt56U)
{
    TestEmberScalarTypeRead<OddSizedInteger<7, false>, ZCL_INT56U_ATTRIBUTE_TYPE>(0xAABB11223344);
}

TEST_F(TestCodegenModelViaMocks, EmberAttributeReadBool)
{
    TestEmberScalarTypeRead<bool, ZCL_BOOLEAN_ATTRIBUTE_TYPE>(true);
    TestEmberScalarTypeRead<bool, ZCL_BOOLEAN_ATTRIBUTE_TYPE>(false);
}

TEST_F(TestCodegenModelViaMocks, EmberAttributeReadInt8U)
{
    TestEmberScalarTypeRead<uint8_t, ZCL_INT8U_ATTRIBUTE_TYPE>(0x12);
}

TEST_F(TestCodegenModelViaMocks, EmberAttributeReadNulls)
{
    TestEmberScalarNullRead<uint8_t, ZCL_INT8U_ATTRIBUTE_TYPE>();
    TestEmberScalarNullRead<uint16_t, ZCL_INT16U_ATTRIBUTE_TYPE>();
    TestEmberScalarNullRead<OddSizedInteger<3, false>, ZCL_INT24U_ATTRIBUTE_TYPE>();
    TestEmberScalarNullRead<uint32_t, ZCL_INT32U_ATTRIBUTE_TYPE>();
    TestEmberScalarNullRead<OddSizedInteger<5, false>, ZCL_INT40U_ATTRIBUTE_TYPE>();
    TestEmberScalarNullRead<OddSizedInteger<6, false>, ZCL_INT48U_ATTRIBUTE_TYPE>();
    TestEmberScalarNullRead<OddSizedInteger<7, false>, ZCL_INT56U_ATTRIBUTE_TYPE>();
    TestEmberScalarNullRead<uint64_t, ZCL_INT64U_ATTRIBUTE_TYPE>();

    TestEmberScalarNullRead<int8_t, ZCL_INT8S_ATTRIBUTE_TYPE>();
    TestEmberScalarNullRead<int16_t, ZCL_INT16S_ATTRIBUTE_TYPE>();
    TestEmberScalarNullRead<OddSizedInteger<3, true>, ZCL_INT24S_ATTRIBUTE_TYPE>();
    TestEmberScalarNullRead<int32_t, ZCL_INT32S_ATTRIBUTE_TYPE>();
    TestEmberScalarNullRead<OddSizedInteger<5, true>, ZCL_INT40S_ATTRIBUTE_TYPE>();
    TestEmberScalarNullRead<OddSizedInteger<6, true>, ZCL_INT48S_ATTRIBUTE_TYPE>();
    TestEmberScalarNullRead<OddSizedInteger<7, true>, ZCL_INT56S_ATTRIBUTE_TYPE>();
    TestEmberScalarNullRead<int64_t, ZCL_INT64S_ATTRIBUTE_TYPE>();

    TestEmberScalarNullRead<bool, ZCL_BOOLEAN_ATTRIBUTE_TYPE>();

    TestEmberScalarNullRead<float, ZCL_SINGLE_ATTRIBUTE_TYPE>();
    TestEmberScalarNullRead<double, ZCL_DOUBLE_ATTRIBUTE_TYPE>();
}

TEST_F(TestCodegenModelViaMocks, EmberAttributeReadErrorReading)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    {
        ReadOperation testRequest(kMockEndpoint3, MockClusterId(4),
                                  MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE));
        testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);

        chip::Testing::SetEmberReadOutput(Protocols::InteractionModel::Status::Failure);

        // Actual read via an encoder
        std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
        ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), Status::Failure);
    }

    {
        ReadOperation testRequest(kMockEndpoint3, MockClusterId(4),
                                  MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE));
        testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);

        chip::Testing::SetEmberReadOutput(Protocols::InteractionModel::Status::Busy);

        // Actual read via an encoder
        std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
        ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), Status::Busy);
    }

    // reset things to success to not affect other tests
    chip::Testing::SetEmberReadOutput(ByteSpan());
}

TEST_F(TestCodegenModelViaMocks, EmberAttributeReadNullOctetString)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    ReadOperation testRequest(kMockEndpoint3, MockClusterId(4),
                              MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE));
    testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);

    // NOTE: This is a pascal string of size 0xFFFF which for null strings is a null marker
    char data[] = "\xFF\xFFInvalid length string is null";
    chip::Testing::SetEmberReadOutput(ByteSpan(reinterpret_cast<const uint8_t *>(data), sizeof(data)));

    // Actual read via an encoder
    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
    ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), CHIP_NO_ERROR);
    ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR);

    // Validate after read
    std::vector<DecodedAttributeData> attribute_data;
    ASSERT_EQ(testRequest.GetEncodedIBs().Decode(attribute_data), CHIP_NO_ERROR);
    ASSERT_EQ(attribute_data.size(), 1u);

    DecodedAttributeData & encodedData = attribute_data[0];
    ASSERT_EQ(encodedData.attributePath, testRequest.GetRequest().path);

    // data element should be null for the given 0xFFFF length
    ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_Null);

    chip::app::DataModel::Nullable<ByteSpan> actual;
    ASSERT_EQ(chip::app::DataModel::Decode(encodedData.dataReader, actual), CHIP_NO_ERROR);
    ASSERT_TRUE(actual.IsNull());
}

TEST_F(TestCodegenModelViaMocks, EmberAttributeReadOctetString)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    ReadOperation testRequest(kMockEndpoint3, MockClusterId(4),
                              MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE));
    testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);

    // NOTE: This is a pascal string, so actual data is "test"
    //       the longer encoding is to make it clear we do not encode the overflow
    char data[] = "\0\0testing here with overflow";
    WriteLe16(data, 4);
    chip::Testing::SetEmberReadOutput(ByteSpan(reinterpret_cast<const uint8_t *>(data), sizeof(data)));

    // Actual read via an encoder
    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
    ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), CHIP_NO_ERROR);
    ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR);

    // Validate after read
    std::vector<DecodedAttributeData> attribute_data;
    ASSERT_EQ(testRequest.GetEncodedIBs().Decode(attribute_data), CHIP_NO_ERROR);
    ASSERT_EQ(attribute_data.size(), 1u);

    const DecodedAttributeData & encodedData = attribute_data[0];
    ASSERT_EQ(encodedData.attributePath, testRequest.GetRequest().path);

    // data element should be a encoded byte string as this is what the attribute type is
    ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_ByteString);
    ByteSpan actual;
    ASSERT_EQ(encodedData.dataReader.Get(actual), CHIP_NO_ERROR);

    ByteSpan expected(reinterpret_cast<const uint8_t *>(data + 2), 4);
    ASSERT_TRUE(actual.data_equal(expected));
}

TEST_F(TestCodegenModelViaMocks, EmberAttributeReadLongOctetString)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    ReadOperation testRequest(kMockEndpoint3, MockClusterId(4),
                              MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_OCTET_STRING_ATTRIBUTE_TYPE));
    testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);

    // NOTE: This is a pascal string, so actual data is "test"
    //       the longer encoding is to make it clear we do not encode the overflow
    const char data[] = "\x04testing here with overflow";
    chip::Testing::SetEmberReadOutput(ByteSpan(reinterpret_cast<const uint8_t *>(data), sizeof(data)));

    // Actual read via an encoder
    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
    ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), CHIP_NO_ERROR);
    ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR);

    // Validate after read
    std::vector<DecodedAttributeData> attribute_data;
    ASSERT_EQ(testRequest.GetEncodedIBs().Decode(attribute_data), CHIP_NO_ERROR);
    ASSERT_EQ(attribute_data.size(), 1u);

    const DecodedAttributeData & encodedData = attribute_data[0];
    ASSERT_EQ(encodedData.attributePath, testRequest.GetRequest().path);

    // data element should be a encoded byte string as this is what the attribute type is
    ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_ByteString);
    ByteSpan actual;
    ASSERT_EQ(encodedData.dataReader.Get(actual), CHIP_NO_ERROR);

    ByteSpan expected(reinterpret_cast<const uint8_t *>(data + 1), 4);
    ASSERT_TRUE(actual.data_equal(expected));
}

TEST_F(TestCodegenModelViaMocks, EmberAttributeReadShortString)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    ReadOperation testRequest(kMockEndpoint3, MockClusterId(4),
                              MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_CHAR_STRING_ATTRIBUTE_TYPE));
    testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);

    // NOTE: This is a pascal string, so actual data is "abcde"
    //       the longer encoding is to make it clear we do not encode the overflow
    char data[] = "\0abcdef...this is the alphabet";
    *data       = 5;
    chip::Testing::SetEmberReadOutput(ByteSpan(reinterpret_cast<const uint8_t *>(data), sizeof(data)));

    // Actual read via an encoder
    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
    ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), CHIP_NO_ERROR);
    ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR);

    // Validate after reading
    std::vector<DecodedAttributeData> attribute_data;
    ASSERT_EQ(testRequest.GetEncodedIBs().Decode(attribute_data), CHIP_NO_ERROR);
    ASSERT_EQ(attribute_data.size(), 1u);

    const DecodedAttributeData & encodedData = attribute_data[0];
    ASSERT_EQ(encodedData.attributePath, testRequest.GetRequest().path);

    // data element should be a encoded byte string as this is what the attribute type is
    ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_UTF8String);
    CharSpan actual;
    ASSERT_EQ(encodedData.dataReader.Get(actual), CHIP_NO_ERROR);
    ASSERT_TRUE(actual.data_equal("abcde"_span));
}

TEST_F(TestCodegenModelViaMocks, EmberAttributeReadLongString)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    ReadOperation testRequest(kMockEndpoint3, MockClusterId(4),
                              MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE));
    testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);

    // NOTE: This is a pascal string, so actual data is "abcde"
    //       the longer encoding is to make it clear we do not encode the overflow
    char data[] = "\0\0abcdef...this is the alphabet";
    WriteLe16(data, 5);
    chip::Testing::SetEmberReadOutput(ByteSpan(reinterpret_cast<const uint8_t *>(data), sizeof(data)));

    // Actual read via an encoder
    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
    ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), CHIP_NO_ERROR);
    ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR);

    // Validate after reading
    std::vector<DecodedAttributeData> attribute_data;
    ASSERT_EQ(testRequest.GetEncodedIBs().Decode(attribute_data), CHIP_NO_ERROR);
    ASSERT_EQ(attribute_data.size(), 1u);

    const DecodedAttributeData & encodedData = attribute_data[0];
    ASSERT_EQ(encodedData.attributePath, testRequest.GetRequest().path);

    // data element should be a encoded byte string as this is what the attribute type is
    ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_UTF8String);
    CharSpan actual;
    ASSERT_EQ(encodedData.dataReader.Get(actual), CHIP_NO_ERROR);
    ASSERT_TRUE(actual.data_equal("abcde"_span));
}

TEST_F(TestCodegenModelViaMocks, AttributeAccessInterfaceStructRead)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    const ConcreteAttributePath kStructPath(kMockEndpoint3, MockClusterId(4),
                                            MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_STRUCT_ATTRIBUTE_TYPE));

    ReadOperation testRequest(kStructPath);
    testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);

    RegisteredAttributeAccessInterface<StructAttributeAccessInterface> aai(kStructPath);

    aai->SetReturnedData(Clusters::UnitTesting::Structs::SimpleStruct::Type{
        .a = 123,
        .b = true,
        .e = "foo"_span,
        .g = 0.5,
        .h = 0.125,
    });

    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
    ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), CHIP_NO_ERROR);
    ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR);

    // Validate after read
    std::vector<DecodedAttributeData> attribute_data;
    ASSERT_EQ(testRequest.GetEncodedIBs().Decode(attribute_data), CHIP_NO_ERROR);
    ASSERT_EQ(attribute_data.size(), 1u);

    DecodedAttributeData & encodedData = attribute_data[0];
    ASSERT_EQ(encodedData.attributePath, testRequest.GetRequest().path);

    ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_Structure);
    Clusters::UnitTesting::Structs::SimpleStruct::DecodableType actual;
    ASSERT_EQ(chip::app::DataModel::Decode(encodedData.dataReader, actual), CHIP_NO_ERROR);

    ASSERT_EQ(actual.a, 123);
    ASSERT_EQ(actual.b, true);
    ASSERT_EQ(actual.g, 0.5);
    ASSERT_EQ(actual.h, 0.125);
    ASSERT_TRUE(actual.e.data_equal("foo"_span));
}

TEST_F(TestCodegenModelViaMocks, AttributeAccessInterfaceReadError)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    const ConcreteAttributePath kStructPath(kMockEndpoint3, MockClusterId(4),
                                            MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_STRUCT_ATTRIBUTE_TYPE));

    ReadOperation testRequest(kStructPath);
    testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);

    RegisteredAttributeAccessInterface<ErrorAccessInterface> aai(kStructPath, CHIP_ERROR_KEY_NOT_FOUND);
    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
    ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), CHIP_ERROR_KEY_NOT_FOUND);
}

TEST_F(TestCodegenModelViaMocks, AttributeAccessInterfaceListRead)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    const ConcreteAttributePath kStructPath(kMockEndpoint3, MockClusterId(4),
                                            MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_ARRAY_ATTRIBUTE_TYPE));

    ReadOperation testRequest(kStructPath);
    testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);

    RegisteredAttributeAccessInterface<ListAttributeAcessInterface> aai(kStructPath);

    constexpr unsigned kDataCount = 5;
    aai->SetReturnedData(Clusters::UnitTesting::Structs::SimpleStruct::Type{
        .b = true,
        .e = "xyz"_span,
        .g = 0.25,
        .h = 0.5,
    });
    aai->SetReturnedDataCount(kDataCount);

    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
    ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), CHIP_NO_ERROR);
    ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR);

    // Validate after read
    std::vector<DecodedAttributeData> attribute_data;
    ASSERT_EQ(testRequest.GetEncodedIBs().Decode(attribute_data), CHIP_NO_ERROR);
    ASSERT_EQ(attribute_data.size(), 1u);

    DecodedAttributeData & encodedData = attribute_data[0];
    ASSERT_EQ(encodedData.attributePath, testRequest.GetRequest().path);

    ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_Array);

    std::vector<Clusters::UnitTesting::Structs::SimpleStruct::DecodableType> items;
    ASSERT_EQ(DecodeList(encodedData.dataReader, items), CHIP_NO_ERROR);

    ASSERT_EQ(items.size(), kDataCount);

    for (unsigned i = 0; i < kDataCount; i++)
    {
        Clusters::UnitTesting::Structs::SimpleStruct::DecodableType & actual = items[i];

        ASSERT_EQ(actual.a, static_cast<uint8_t>(i & 0xFF));
        ASSERT_EQ(actual.b, true);
        ASSERT_EQ(actual.g, 0.25);
        ASSERT_EQ(actual.h, 0.5);
        ASSERT_TRUE(actual.e.data_equal("xyz"_span));
    }
}

TEST_F(TestCodegenModelViaMocks, AttributeAccessInterfaceListOverflowRead)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    const ConcreteAttributePath kStructPath(kMockEndpoint3, MockClusterId(4),
                                            MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_ARRAY_ATTRIBUTE_TYPE));

    ReadOperation testRequest(kStructPath);
    testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);
    RegisteredAttributeAccessInterface<ListAttributeAcessInterface> aai(kStructPath);

    constexpr unsigned kDataCount = 1024;
    aai->SetReturnedData(Clusters::UnitTesting::Structs::SimpleStruct::Type{
        .b = true,
        .e = "thisislongertofillupfaster"_span,
        .g = 0.25,
        .h = 0.5,
    });
    aai->SetReturnedDataCount(kDataCount);

    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
    // NOTE: overflow, however data should be valid. Technically both NO_MEMORY and BUFFER_TOO_SMALL
    // should be ok here, however we know buffer-too-small is the error in this case hence
    // the compare (easier to write the test and read the output)
    ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), CHIP_ERROR_BUFFER_TOO_SMALL);
    ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR);

    // Validate after read
    std::vector<DecodedAttributeData> attribute_data;
    ASSERT_EQ(testRequest.GetEncodedIBs().Decode(attribute_data), CHIP_NO_ERROR);
    ASSERT_EQ(attribute_data.size(), 1u);

    DecodedAttributeData & encodedData = attribute_data[0];
    ASSERT_EQ(encodedData.attributePath, testRequest.GetRequest().path);

    ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_Array);

    std::vector<Clusters::UnitTesting::Structs::SimpleStruct::DecodableType> items;
    ASSERT_EQ(DecodeList(encodedData.dataReader, items), CHIP_NO_ERROR);

    // On last check, 16 items can be encoded. Set some non-zero range to be enforced here that
    // SOME list items are actually encoded. Actual lower bound here IS ARBITRARY and was picked
    // to just ensure non-zero item count for checks.
    ASSERT_GT(items.size(), 5u);
    ASSERT_LT(items.size(), kDataCount);

    for (unsigned i = 0; i < items.size(); i++)
    {
        Clusters::UnitTesting::Structs::SimpleStruct::DecodableType & actual = items[i];

        ASSERT_EQ(actual.a, static_cast<uint8_t>(i & 0xFF));
        ASSERT_EQ(actual.b, true);
        ASSERT_EQ(actual.g, 0.25);
        ASSERT_EQ(actual.h, 0.5);
        ASSERT_TRUE(actual.e.data_equal("thisislongertofillupfaster"_span));
    }
}

TEST_F(TestCodegenModelViaMocks, AttributeAccessInterfaceListIncrementalRead)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    const ConcreteAttributePath kStructPath(kMockEndpoint3, MockClusterId(4),
                                            MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_ARRAY_ATTRIBUTE_TYPE));

    ReadOperation testRequest(kStructPath);
    testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);
    RegisteredAttributeAccessInterface<ListAttributeAcessInterface> aai(kStructPath);

    constexpr unsigned kDataCount        = 1024;
    constexpr unsigned kEncodeIndexStart = 101;
    aai->SetReturnedData(Clusters::UnitTesting::Structs::SimpleStruct::Type{
        .b = true,
        .e = "thisislongertofillupfaster"_span,
        .g = 0.25,
        .h = 0.5,
    });
    aai->SetReturnedDataCount(kDataCount);

    AttributeEncodeState encodeState;
    encodeState.SetCurrentEncodingListIndex(kEncodeIndexStart);

    std::unique_ptr<AttributeValueEncoder> encoder =
        testRequest.StartEncoding(ReadOperation::EncodingParams().SetEncodingState(encodeState));

    // NOTE: overflow, however data should be valid. Technically both NO_MEMORY and BUFFER_TOO_SMALL
    // should be ok here, however we know buffer-too-small is the error in this case hence
    // the compare (easier to write the test and read the output)
    ASSERT_EQ(model.ReadAttribute(testRequest.GetRequest(), *encoder), CHIP_ERROR_BUFFER_TOO_SMALL);
    ASSERT_EQ(testRequest.FinishEncoding(), CHIP_NO_ERROR);

    // Validate after read
    std::vector<DecodedAttributeData> attribute_data;
    ASSERT_EQ(testRequest.GetEncodedIBs().Decode(attribute_data), CHIP_NO_ERROR);

    // Incremental encodes are separate list items, repeated
    // actual size IS ARBITRARY (current test sets it at 11)
    ASSERT_GT(attribute_data.size(), 3u);

    for (unsigned i = 0; i < attribute_data.size(); i++)
    {
        DecodedAttributeData & encodedData = attribute_data[i];
        ASSERT_EQ(encodedData.attributePath.mEndpointId, testRequest.GetRequest().path.mEndpointId);
        ASSERT_EQ(encodedData.attributePath.mClusterId, testRequest.GetRequest().path.mClusterId);
        ASSERT_EQ(encodedData.attributePath.mAttributeId, testRequest.GetRequest().path.mAttributeId);
        ASSERT_EQ(encodedData.attributePath.mListOp, ConcreteDataAttributePath::ListOperation::AppendItem);

        // individual structures encoded in each item
        ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_Structure);

        Clusters::UnitTesting::Structs::SimpleStruct::DecodableType actual;
        ASSERT_EQ(chip::app::DataModel::Decode(encodedData.dataReader, actual), CHIP_NO_ERROR);

        ASSERT_EQ(actual.a, static_cast<uint8_t>((i + kEncodeIndexStart) & 0xFF));
        ASSERT_EQ(actual.b, true);
        ASSERT_EQ(actual.g, 0.25);
        ASSERT_EQ(actual.h, 0.5);
        ASSERT_TRUE(actual.e.data_equal("thisislongertofillupfaster"_span));
    }
}

// reading attributes is a LOT of boilerplate. This just makes tests more readable.
static CHIP_ERROR ReadU32Attribute(DataModel::Provider & provider, const ConcreteAttributePath & path, uint32_t & value)
{

    ReadOperation testRequest(path);
    testRequest.SetSubjectDescriptor(kAdminSubjectDescriptor);

    std::unique_ptr<AttributeValueEncoder> encoder = testRequest.StartEncoding();
    ReturnErrorOnFailure(provider.ReadAttribute(testRequest.GetRequest(), *encoder).GetUnderlyingError());
    ReturnErrorOnFailure(testRequest.FinishEncoding());

    std::vector<DecodedAttributeData> attribute_data;

    ReturnErrorOnFailure(testRequest.GetEncodedIBs().Decode(attribute_data));
    VerifyOrReturnError(attribute_data.size() == 1u, CHIP_ERROR_INCORRECT_STATE);

    DecodedAttributeData & encodedData = attribute_data[0];

    VerifyOrReturnError(encodedData.attributePath == testRequest.GetRequest().path, CHIP_ERROR_INCORRECT_STATE);

    return chip::app::DataModel::Decode<uint32_t>(encodedData.dataReader, value);
}

TEST_F(TestCodegenModelViaMocks, AttributeAccessInterfaceTakesPrecedenceOverServerClusterInterface)
{
    // For backwards compatibility, we want AAI requests to be sent first and override SCI
    // The test verifies this by adding a "error-out" AAI that overrides success SCI
    TestServerClusterContext testContext;

    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;

    model.SetPersistentStorageDelegate(&testContext.StorageDelegate());
    ASSERT_EQ(model.Startup(testContext.ImContext()), CHIP_NO_ERROR);

    // It is important to have kTestClusterPath be valid ember paths (so we have metadata for them)
    const ConcreteClusterPath kTestClusterPath(kMockEndpoint3, MockClusterId(4));
    const ConcreteAttributePath kTestAttributePath(kTestClusterPath.mEndpointId, kTestClusterPath.mClusterId,
                                                   kAttributeIdFakeAllowsWrite);
    FakeDefaultServerCluster fakeClusterServer(kTestClusterPath);
    ServerClusterRegistration registration(fakeClusterServer);

    // SCI registered for kTestClusterPath, and this will work by a R/W for kTestAttributePath
    ASSERT_EQ(model.Registry().Register(registration), CHIP_NO_ERROR);

    {
        // AAI registered that fails R/W
        RegisteredAttributeAccessInterface<UnsupportedReadAccessInterface> aai(kTestAttributePath);

        // Reads fail because AAI
        uint32_t value = 0;
        ASSERT_EQ(ReadU32Attribute(model, kTestAttributePath, value), CHIP_IM_GLOBAL_STATUS(UnsupportedRead));

        // Writes fail because AAI.
        WriteOperation test(kTestAttributePath);
        test.SetSubjectDescriptor(kAdminSubjectDescriptor);
        AttributeValueDecoder decoder = test.DecoderFor(value);

        ASSERT_FALSE(model.WriteAttribute(test.GetRequest(), decoder).IsSuccess());
    }

    {
        // now that AAI is out of the picture, SCI will read/write things ok
        uint32_t value = 0;
        ASSERT_EQ(ReadU32Attribute(model, kTestAttributePath, value), CHIP_NO_ERROR);

        WriteOperation test(kTestAttributePath);
        test.SetSubjectDescriptor(kAdminSubjectDescriptor);
        AttributeValueDecoder decoder = test.DecoderFor(value);

        // write should succeed
        ASSERT_TRUE(model.WriteAttribute(test.GetRequest(), decoder).IsSuccess());
    }

    EXPECT_SUCCESS(model.Registry().Unregister(&fakeClusterServer));
}

TEST_F(TestCodegenModelViaMocks, AAISkippedIfNoEmberMetadata)
{
    // For backwards compatibility, we want AAI requests to be sent first and override SCI
    // The test verifies this by adding a "error-out" AAI that overrides success SCI
    TestServerClusterContext testContext;

    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;

    model.SetPersistentStorageDelegate(&testContext.StorageDelegate());
    ASSERT_EQ(model.Startup(testContext.ImContext()), CHIP_NO_ERROR);

    // These paths are NOT valid for AAI, so AAI is skipped
    const ConcreteClusterPath kTestClusterPath(kMockEndpoint1, MockClusterId(1));
    const ConcreteAttributePath kTestAttributePath(kTestClusterPath.mEndpointId, kTestClusterPath.mClusterId,
                                                   kAttributeIdFakeAllowsWrite);
    FakeDefaultServerCluster fakeClusterServer(kTestClusterPath);
    ServerClusterRegistration registration(fakeClusterServer);

    // SCI registered for kTestClusterPath, and this will work by a R/W for kTestAttributePath
    ASSERT_EQ(model.Registry().Register(registration), CHIP_NO_ERROR);

    {
        // AAI registered that fails R/W, however because ember metadata path is not valid
        // this AAI does NOT take effect
        RegisteredAttributeAccessInterface<UnsupportedReadAccessInterface> aai(kTestAttributePath);

        // now that AAI is out of the picture, SCI will read/write things ok
        uint32_t value = 0;
        ASSERT_EQ(ReadU32Attribute(model, kTestAttributePath, value), CHIP_NO_ERROR);

        WriteOperation test(kTestAttributePath);
        test.SetSubjectDescriptor(kAdminSubjectDescriptor);
        AttributeValueDecoder decoder = test.DecoderFor(value);

        // write should succeed
        ASSERT_TRUE(model.WriteAttribute(test.GetRequest(), decoder).IsSuccess());
    }

    EXPECT_SUCCESS(model.Registry().Unregister(&fakeClusterServer));
}

TEST_F(TestCodegenModelViaMocks, EmberAttributeWriteBasicTypes)
{
    TestEmberScalarTypeWrite<uint8_t, ZCL_INT8U_ATTRIBUTE_TYPE>(0x12);
    TestEmberScalarTypeWrite<uint16_t, ZCL_ENUM16_ATTRIBUTE_TYPE>(0x1234);
    TestEmberScalarTypeWrite<OddSizedInteger<3, false>, ZCL_INT24U_ATTRIBUTE_TYPE>(0x112233);
    TestEmberScalarTypeWrite<uint32_t, ZCL_INT32U_ATTRIBUTE_TYPE>(0x11223344);
    TestEmberScalarTypeWrite<OddSizedInteger<5, false>, ZCL_INT40U_ATTRIBUTE_TYPE>(0x1122334455ULL);
    TestEmberScalarTypeWrite<OddSizedInteger<6, false>, ZCL_INT48U_ATTRIBUTE_TYPE>(0x112233445566ULL);
    TestEmberScalarTypeWrite<OddSizedInteger<7, false>, ZCL_INT56U_ATTRIBUTE_TYPE>(0x11223344556677ULL);
    TestEmberScalarTypeWrite<uint64_t, ZCL_INT64U_ATTRIBUTE_TYPE>(0x1122334455667788ULL);

    TestEmberScalarTypeWrite<int8_t, ZCL_INT8S_ATTRIBUTE_TYPE>(-10);
    TestEmberScalarTypeWrite<int16_t, ZCL_INT16S_ATTRIBUTE_TYPE>(-123);
    TestEmberScalarTypeWrite<OddSizedInteger<3, true>, ZCL_INT24S_ATTRIBUTE_TYPE>(-1234);
    TestEmberScalarTypeWrite<int32_t, ZCL_INT32S_ATTRIBUTE_TYPE>(-12345);
    TestEmberScalarTypeWrite<OddSizedInteger<5, true>, ZCL_INT40S_ATTRIBUTE_TYPE>(-123456);
    TestEmberScalarTypeWrite<OddSizedInteger<6, true>, ZCL_INT48S_ATTRIBUTE_TYPE>(-1234567);
    TestEmberScalarTypeWrite<OddSizedInteger<7, true>, ZCL_INT56S_ATTRIBUTE_TYPE>(-12345678);
    TestEmberScalarTypeWrite<int64_t, ZCL_INT64S_ATTRIBUTE_TYPE>(-123456789);

    TestEmberScalarTypeWrite<bool, ZCL_BOOLEAN_ATTRIBUTE_TYPE>(true);
    TestEmberScalarTypeWrite<bool, ZCL_BOOLEAN_ATTRIBUTE_TYPE>(false);
    TestEmberScalarTypeWrite<float, ZCL_SINGLE_ATTRIBUTE_TYPE>(0.625);
    TestEmberScalarTypeWrite<double, ZCL_DOUBLE_ATTRIBUTE_TYPE>(0.625);
}

TEST_F(TestCodegenModelViaMocks, EmberAttributeWriteInvalidValueToNullable)
{
    TestEmberScalarTypeWriteNullValueToNullable<uint8_t, ZCL_INT8U_ATTRIBUTE_TYPE>();
    TestEmberScalarTypeWriteNullValueToNullable<uint16_t, ZCL_ENUM16_ATTRIBUTE_TYPE>();
    TestEmberScalarTypeWriteNullValueToNullable<OddSizedInteger<3, false>, ZCL_INT24U_ATTRIBUTE_TYPE>();
    TestEmberScalarTypeWriteNullValueToNullable<uint32_t, ZCL_INT32U_ATTRIBUTE_TYPE>();
    TestEmberScalarTypeWriteNullValueToNullable<OddSizedInteger<5, false>, ZCL_INT40U_ATTRIBUTE_TYPE>();
    TestEmberScalarTypeWriteNullValueToNullable<OddSizedInteger<6, false>, ZCL_INT48U_ATTRIBUTE_TYPE>();
    TestEmberScalarTypeWriteNullValueToNullable<OddSizedInteger<7, false>, ZCL_INT56U_ATTRIBUTE_TYPE>();
    TestEmberScalarTypeWriteNullValueToNullable<uint64_t, ZCL_INT64U_ATTRIBUTE_TYPE>();

    TestEmberScalarTypeWriteNullValueToNullable<int8_t, ZCL_INT8S_ATTRIBUTE_TYPE>();
    TestEmberScalarTypeWriteNullValueToNullable<int16_t, ZCL_INT16S_ATTRIBUTE_TYPE>();
    TestEmberScalarTypeWriteNullValueToNullable<OddSizedInteger<3, true>, ZCL_INT24S_ATTRIBUTE_TYPE>();
    TestEmberScalarTypeWriteNullValueToNullable<int32_t, ZCL_INT32S_ATTRIBUTE_TYPE>();
    TestEmberScalarTypeWriteNullValueToNullable<OddSizedInteger<5, true>, ZCL_INT40S_ATTRIBUTE_TYPE>();
    TestEmberScalarTypeWriteNullValueToNullable<OddSizedInteger<6, true>, ZCL_INT48S_ATTRIBUTE_TYPE>();
    TestEmberScalarTypeWriteNullValueToNullable<OddSizedInteger<7, true>, ZCL_INT56S_ATTRIBUTE_TYPE>();
    TestEmberScalarTypeWriteNullValueToNullable<int64_t, ZCL_INT64S_ATTRIBUTE_TYPE>();

    TestEmberScalarTypeWriteNullValueToNullable<bool, ZCL_BOOLEAN_ATTRIBUTE_TYPE>();
    TestEmberScalarTypeWriteNullValueToNullable<bool, ZCL_BOOLEAN_ATTRIBUTE_TYPE>();
    TestEmberScalarTypeWriteNullValueToNullable<float, ZCL_SINGLE_ATTRIBUTE_TYPE>();
    TestEmberScalarTypeWriteNullValueToNullable<double, ZCL_DOUBLE_ATTRIBUTE_TYPE>();
}

TEST_F(TestCodegenModelViaMocks, EmberTestWriteReservedNullPlaceholderToNullable)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    WriteOperation test(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_INT32U_ATTRIBUTE_TYPE));
    test.SetSubjectDescriptor(kAdminSubjectDescriptor);

    using NumericType             = NumericAttributeTraits<uint32_t>;
    using NullableType            = chip::app::DataModel::Nullable<typename NumericType::WorkingType>;
    AttributeValueDecoder decoder = test.DecoderFor<NullableType>(0xFFFFFFFF);

    // write should fail: we are trying to write null which is out of range
    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), Status::ConstraintError);
}

TEST_F(TestCodegenModelViaMocks, EmberTestWriteOutOfRepresentableRangeOddIntegerNonNullable)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    WriteOperation test(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_INT24U_ATTRIBUTE_TYPE));
    test.SetSubjectDescriptor(kAdminSubjectDescriptor);

    using NumericType             = NumericAttributeTraits<uint32_t>;
    using NullableType            = chip::app::DataModel::Nullable<typename NumericType::WorkingType>;
    AttributeValueDecoder decoder = test.DecoderFor<NullableType>(0x1223344);

    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_IM_GLOBAL_STATUS(ConstraintError));
}

TEST_F(TestCodegenModelViaMocks, EmberTestWriteOutOfRepresentableRangeOddIntegerNullable)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    WriteOperation test(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_INT24U_ATTRIBUTE_TYPE));
    test.SetSubjectDescriptor(kAdminSubjectDescriptor);

    using NumericType             = NumericAttributeTraits<uint32_t>;
    using NullableType            = chip::app::DataModel::Nullable<typename NumericType::WorkingType>;
    AttributeValueDecoder decoder = test.DecoderFor<NullableType>(0x1223344);

    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_IM_GLOBAL_STATUS(ConstraintError));
}

TEST_F(TestCodegenModelViaMocks, EmberAttributeWriteBasicTypesLowestValue)
{
    TestEmberScalarTypeWrite<int8_t, ZCL_INT8S_ATTRIBUTE_TYPE>(-127);
    TestEmberScalarTypeWrite<int16_t, ZCL_INT16S_ATTRIBUTE_TYPE>(-32767);
    TestEmberScalarTypeWrite<OddSizedInteger<3, true>, ZCL_INT24S_ATTRIBUTE_TYPE>(-8388607);
    TestEmberScalarTypeWrite<int32_t, ZCL_INT32S_ATTRIBUTE_TYPE>(-2147483647);
    TestEmberScalarTypeWrite<OddSizedInteger<5, true>, ZCL_INT40S_ATTRIBUTE_TYPE>(-549755813887);
    TestEmberScalarTypeWrite<OddSizedInteger<6, true>, ZCL_INT48S_ATTRIBUTE_TYPE>(-140737488355327);
    TestEmberScalarTypeWrite<OddSizedInteger<7, true>, ZCL_INT56S_ATTRIBUTE_TYPE>(-36028797018963967);
    TestEmberScalarTypeWrite<int64_t, ZCL_INT64S_ATTRIBUTE_TYPE>(-9223372036854775807);
}

TEST_F(TestCodegenModelViaMocks, EmberAttributeWriteNulls)
{
    TestEmberScalarNullWrite<uint8_t, ZCL_INT8U_ATTRIBUTE_TYPE>();
    TestEmberScalarNullWrite<uint16_t, ZCL_ENUM16_ATTRIBUTE_TYPE>();
    TestEmberScalarNullWrite<OddSizedInteger<3, false>, ZCL_INT24U_ATTRIBUTE_TYPE>();
    TestEmberScalarNullWrite<uint32_t, ZCL_INT32U_ATTRIBUTE_TYPE>();
    TestEmberScalarNullWrite<OddSizedInteger<5, false>, ZCL_INT40U_ATTRIBUTE_TYPE>();
    TestEmberScalarNullWrite<OddSizedInteger<6, false>, ZCL_INT48U_ATTRIBUTE_TYPE>();
    TestEmberScalarNullWrite<OddSizedInteger<7, false>, ZCL_INT56U_ATTRIBUTE_TYPE>();
    TestEmberScalarNullWrite<uint64_t, ZCL_INT64U_ATTRIBUTE_TYPE>();

    TestEmberScalarNullWrite<int8_t, ZCL_INT8S_ATTRIBUTE_TYPE>();
    TestEmberScalarNullWrite<int16_t, ZCL_INT16S_ATTRIBUTE_TYPE>();
    TestEmberScalarNullWrite<OddSizedInteger<3, true>, ZCL_INT24S_ATTRIBUTE_TYPE>();
    TestEmberScalarNullWrite<int32_t, ZCL_INT32S_ATTRIBUTE_TYPE>();
    TestEmberScalarNullWrite<OddSizedInteger<5, true>, ZCL_INT40S_ATTRIBUTE_TYPE>();
    TestEmberScalarNullWrite<OddSizedInteger<6, true>, ZCL_INT48S_ATTRIBUTE_TYPE>();
    TestEmberScalarNullWrite<OddSizedInteger<7, true>, ZCL_INT56S_ATTRIBUTE_TYPE>();
    TestEmberScalarNullWrite<int64_t, ZCL_INT64S_ATTRIBUTE_TYPE>();
    TestEmberScalarNullWrite<bool, ZCL_BOOLEAN_ATTRIBUTE_TYPE>();
    TestEmberScalarNullWrite<float, ZCL_SINGLE_ATTRIBUTE_TYPE>();
    TestEmberScalarNullWrite<double, ZCL_DOUBLE_ATTRIBUTE_TYPE>();
}

TEST_F(TestCodegenModelViaMocks, EmberAttributeWriteShortString)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    WriteOperation test(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_CHAR_STRING_ATTRIBUTE_TYPE));
    test.SetSubjectDescriptor(kAdminSubjectDescriptor);

    AttributeValueDecoder decoder = test.DecoderFor<CharSpan>("hello world"_span);

    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_NO_ERROR);
    chip::ByteSpan writtenData = GetEmberBuffer();
    chip::CharSpan asCharSpan(reinterpret_cast<const char *>(writtenData.data()), writtenData[0] + 1);
    ASSERT_TRUE(asCharSpan.data_equal("\x0Bhello world"_span));
}

TEST_F(TestCodegenModelViaMocks, EmberAttributeWriteLongStringOutOfBounds)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    WriteOperation test(kMockEndpoint3, MockClusterId(4),
                        MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE));
    test.SetSubjectDescriptor(kAdminSubjectDescriptor);

    // Mocks allow for 16 bytes only by default for string attributes
    AttributeValueDecoder decoder = test.DecoderFor<CharSpan>(
        "this is a very long string that will be longer than the default attribute size for our mocks"_span);

    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), Status::InvalidValue);
}

TEST_F(TestCodegenModelViaMocks, EmberAttributeWriteLongString)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    WriteOperation test(kMockEndpoint3, MockClusterId(4),
                        MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE));
    test.SetSubjectDescriptor(kAdminSubjectDescriptor);

    AttributeValueDecoder decoder = test.DecoderFor<CharSpan>("text"_span);

    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_NO_ERROR);
    chip::ByteSpan writtenData = GetEmberBuffer();

    uint16_t len = ReadLe16(writtenData.data());
    EXPECT_EQ(len, 4);
    chip::CharSpan asCharSpan(reinterpret_cast<const char *>(writtenData.data() + 2), 4);

    ASSERT_TRUE(asCharSpan.data_equal("text"_span));
}

TEST_F(TestCodegenModelViaMocks, EmberAttributeWriteNullableLongStringValue)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    WriteOperation test(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE));
    test.SetSubjectDescriptor(kAdminSubjectDescriptor);

    AttributeValueDecoder decoder =
        test.DecoderFor<chip::app::DataModel::Nullable<CharSpan>>(chip::app::DataModel::MakeNullable("text"_span));

    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_NO_ERROR);
    chip::ByteSpan writtenData = GetEmberBuffer();

    uint16_t len = ReadLe16(writtenData.data());
    EXPECT_EQ(len, 4);
    chip::CharSpan asCharSpan(reinterpret_cast<const char *>(writtenData.data() + 2), 4);

    ASSERT_TRUE(asCharSpan.data_equal("text"_span));
}

TEST_F(TestCodegenModelViaMocks, EmberAttributeWriteLongNullableStringNull)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    WriteOperation test(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE));
    test.SetSubjectDescriptor(kAdminSubjectDescriptor);

    AttributeValueDecoder decoder =
        test.DecoderFor<chip::app::DataModel::Nullable<CharSpan>>(chip::app::DataModel::Nullable<CharSpan>());

    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_NO_ERROR);
    chip::ByteSpan writtenData = GetEmberBuffer();
    ASSERT_EQ(writtenData[0], 0xFF);
    ASSERT_EQ(writtenData[1], 0xFF);
}

TEST_F(TestCodegenModelViaMocks, EmberAttributeWriteShortBytes)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    WriteOperation test(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_OCTET_STRING_ATTRIBUTE_TYPE));
    test.SetSubjectDescriptor(kAdminSubjectDescriptor);

    uint8_t buffer[] = { 11, 12, 13 };

    AttributeValueDecoder decoder = test.DecoderFor<ByteSpan>(ByteSpan(buffer));

    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_NO_ERROR);
    chip::ByteSpan writtenData = GetEmberBuffer();

    EXPECT_EQ(writtenData[0], 3u);
    EXPECT_EQ(writtenData[1], 11u);
    EXPECT_EQ(writtenData[2], 12u);
    EXPECT_EQ(writtenData[3], 13u);
}

TEST_F(TestCodegenModelViaMocks, EmberAttributeWriteLongBytes)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    WriteOperation test(kMockEndpoint3, MockClusterId(4),
                        MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE));
    test.SetSubjectDescriptor(kAdminSubjectDescriptor);

    uint8_t buffer[] = { 11, 12, 13 };

    AttributeValueDecoder decoder = test.DecoderFor<ByteSpan>(ByteSpan(buffer));

    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_NO_ERROR);
    chip::ByteSpan writtenData = GetEmberBuffer();

    uint16_t len = ReadLe16(writtenData.data());
    EXPECT_EQ(len, 3);

    EXPECT_EQ(writtenData[2], 11u);
    EXPECT_EQ(writtenData[3], 12u);
    EXPECT_EQ(writtenData[4], 13u);
}

TEST_F(TestCodegenModelViaMocks, EmberWriteFailure)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    WriteOperation test(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_INT32S_ATTRIBUTE_TYPE));
    test.SetSubjectDescriptor(kAdminSubjectDescriptor);

    {
        AttributeValueDecoder decoder = test.DecoderFor<int32_t>(1234);
        chip::Testing::SetEmberReadOutput(Protocols::InteractionModel::Status::Failure);
        ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), Status::Failure);
    }
    {
        AttributeValueDecoder decoder = test.DecoderFor<int32_t>(1234);
        chip::Testing::SetEmberReadOutput(Protocols::InteractionModel::Status::Busy);
        ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), Status::Busy);
    }
    // reset things to success to not affect other tests
    chip::Testing::SetEmberReadOutput(ByteSpan());
}

TEST_F(TestCodegenModelViaMocks, EmberWriteAttributeAccessInterfaceTest)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    const ConcreteAttributePath kStructPath(kMockEndpoint3, MockClusterId(4),
                                            MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_STRUCT_ATTRIBUTE_TYPE));
    RegisteredAttributeAccessInterface<StructAttributeAccessInterface> aai(kStructPath);

    WriteOperation test(kStructPath);
    test.SetSubjectDescriptor(kAdminSubjectDescriptor);

    Clusters::UnitTesting::Structs::SimpleStruct::Type testValue{
        .a = 112,
        .b = true,
        .e = "aai_write_test"_span,
        .g = 0.5,
        .h = 0.125,
    };

    AttributeValueDecoder decoder = test.DecoderFor(testValue);
    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_NO_ERROR);

    EXPECT_EQ(aai->GetData().a, 112);
    EXPECT_TRUE(aai->GetData().e.data_equal("aai_write_test"_span));

    // AAI marks dirty paths
    ASSERT_EQ(model.ChangeListener().DirtyList().size(), 1u);
    EXPECT_EQ(model.ChangeListener().DirtyList()[0],
              AttributePathParams(kStructPath.mEndpointId, kStructPath.mClusterId, kStructPath.mAttributeId));

    // AAI does not prevent read/write of regular attributes
    // validate that once AAI is added, we still can go through writing regular bits (i.e.
    // AAI returning "unknown" has fallback to ember)
    TestEmberScalarTypeWrite<uint32_t, ZCL_INT32U_ATTRIBUTE_TYPE>(4321);
    TestEmberScalarNullWrite<int64_t, ZCL_INT64S_ATTRIBUTE_TYPE>();
}

TEST_F(TestCodegenModelViaMocks, EmberInvokeTest)
{
    // Ember invoke is fully code-generated - there is a single function for Dispatch
    // that will do a `switch` on the path elements and invoke a corresponding `emberAf*`
    // callback.
    //
    // The only thing that can be validated is that this `DispatchSingleClusterCommand`
    // is actually invoked.

    UseMockNodeConfig config(gTestNodeConfig);
    chip::app::CodegenDataModelProvider model;

    {
        const ConcreteCommandPath kCommandPath(kMockEndpoint1, MockClusterId(1), kMockCommandId1);
        const InvokeRequest kInvokeRequest{ .path = kCommandPath };
        chip::TLV::TLVReader tlvReader;

        const uint32_t kDispatchCountPre = chip::Testing::DispatchCount();

        // Using a handler set to nullptr as it is not used by the impl
        ASSERT_EQ(model.InvokeCommand(kInvokeRequest, tlvReader, /* handler = */ nullptr), std::nullopt);

        EXPECT_EQ(chip::Testing::DispatchCount(), kDispatchCountPre + 1); // single dispatch
        EXPECT_EQ(chip::Testing::GetLastDispatchPath(), kCommandPath);    // for the right path
    }

    {
        const ConcreteCommandPath kCommandPath(kMockEndpoint1, MockClusterId(1), kMockCommandId2);
        const InvokeRequest kInvokeRequest{ .path = kCommandPath };
        chip::TLV::TLVReader tlvReader;

        const uint32_t kDispatchCountPre = chip::Testing::DispatchCount();

        // Using a handler set to nullpotr as it is not used by the impl
        ASSERT_EQ(model.InvokeCommand(kInvokeRequest, tlvReader, /* handler = */ nullptr), std::nullopt);

        EXPECT_EQ(chip::Testing::DispatchCount(), kDispatchCountPre + 1); // single dispatch
        EXPECT_EQ(chip::Testing::GetLastDispatchPath(), kCommandPath);    // for the right path
    }
}

TEST_F(TestCodegenModelViaMocks, EmberWriteAttributeAccessInterfaceReturningError)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    const ConcreteAttributePath kStructPath(kMockEndpoint3, MockClusterId(4),
                                            MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_STRUCT_ATTRIBUTE_TYPE));
    RegisteredAttributeAccessInterface<ErrorAccessInterface> aai(kStructPath, CHIP_ERROR_KEY_NOT_FOUND);

    WriteOperation test(kStructPath);
    test.SetSubjectDescriptor(kAdminSubjectDescriptor);

    Clusters::UnitTesting::Structs::SimpleStruct::Type testValue{
        .a = 112,
        .b = true,
        .e = "aai_write_test"_span,
        .g = 0.5,
        .h = 0.125,
    };

    AttributeValueDecoder decoder = test.DecoderFor(testValue);
    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_ERROR_KEY_NOT_FOUND);
    ASSERT_TRUE(model.ChangeListener().DirtyList().empty());
}

TEST_F(TestCodegenModelViaMocks, EmberWriteInvalidDataType)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;
    ScopedMockAccessControl accessControl;

    const ConcreteAttributePath kStructPath(kMockEndpoint3, MockClusterId(4),
                                            MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_STRUCT_ATTRIBUTE_TYPE));

    WriteOperation test(kStructPath);
    test.SetSubjectDescriptor(kAdminSubjectDescriptor);

    Clusters::UnitTesting::Structs::SimpleStruct::Type testValue{
        .a = 112,
        .b = true,
        .e = "aai_write_test"_span,
        .g = 0.5,
        .h = 0.125,
    };

    AttributeValueDecoder decoder = test.DecoderFor(testValue);

    // Embed specifically DOES NOT support structures.
    // Without AAI, we expect a data type error (translated to failure)
    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), Status::Failure);
    ASSERT_TRUE(model.ChangeListener().DirtyList().empty());
}

TEST_F(TestCodegenModelViaMocks, DeviceTypeIteration)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;

    // Mock endpoint 1 has 3 device types
    ReadOnlyBufferBuilder<DataModel::DeviceTypeEntry> builder;
    ASSERT_EQ(model.DeviceTypes(kMockEndpoint1, builder), CHIP_NO_ERROR);
    auto deviceTypes = builder.TakeBuffer();
    ASSERT_EQ(deviceTypes.size(), 3u);

    const DeviceTypeEntry expected1[] = {
        { .deviceTypeId = kDeviceTypeId1, .deviceTypeRevision = kDeviceTypeId1Version },
        { .deviceTypeId = kDeviceTypeId2, .deviceTypeRevision = kDeviceTypeId2Version },
        { .deviceTypeId = kDeviceTypeId3, .deviceTypeRevision = kDeviceTypeId3Version },
    };
    for (unsigned i = 0; i < 3; i++)
    {
        ASSERT_EQ(deviceTypes[i], expected1[i]);
    }

    // Mock endpoint 2 has 1 device types
    ASSERT_TRUE(builder.IsEmpty()); // ownership taken above, we start fresh
    ASSERT_EQ(model.DeviceTypes(kMockEndpoint2, builder), CHIP_NO_ERROR);
    deviceTypes = builder.TakeBuffer();
    ASSERT_EQ(deviceTypes.size(), 1u);
    const DeviceTypeEntry expected2 = { .deviceTypeId = kDeviceTypeId2, .deviceTypeRevision = kDeviceTypeId2Version };
    ASSERT_EQ(deviceTypes[0], expected2);

    // empty endpoint works
    ASSERT_TRUE(builder.IsEmpty()); // ownership taken above, we start fresh
    ASSERT_EQ(model.DeviceTypes(kMockEndpoint3, builder), CHIP_NO_ERROR);
    ASSERT_TRUE(builder.IsEmpty());
    ASSERT_TRUE(builder.TakeBuffer().empty());
}

TEST_F(TestCodegenModelViaMocks, ServerClusterInterfacesWrite)
{
    TestServerClusterContext testContext;

    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;

    model.SetPersistentStorageDelegate(&testContext.StorageDelegate());
    ASSERT_EQ(model.Startup(testContext.ImContext()), CHIP_NO_ERROR);

    const ConcreteClusterPath kTestClusterPath(kMockEndpoint1, MockClusterId(2));
    FakeDefaultServerCluster fakeClusterServer(kTestClusterPath);
    ServerClusterRegistration registration(fakeClusterServer);

    ASSERT_EQ(model.Registry().Register(registration), CHIP_NO_ERROR);

    // Write just works
    {
        WriteOperation test(kTestClusterPath.mEndpointId, kTestClusterPath.mClusterId, kAttributeIdFakeAllowsWrite);
        test.SetSubjectDescriptor(kAdminSubjectDescriptor);

        AttributeValueDecoder decoder = test.DecoderFor<uint32_t>(1234);

        std::optional<ActionReturnStatus> result = model.WriteAttribute(test.GetRequest(), decoder);
        ASSERT_TRUE(result.has_value() && result->IsSuccess());
    }

    // Write with a data version works
    {
        WriteOperation test(kTestClusterPath.mEndpointId, kTestClusterPath.mClusterId, kAttributeIdFakeAllowsWrite);
        test.SetSubjectDescriptor(kAdminSubjectDescriptor);
        test.SetDataVersion(MakeOptional(fakeClusterServer.GetDataVersion(kTestClusterPath)));

        AttributeValueDecoder decoder = test.DecoderFor<uint32_t>(1234);

        std::optional<ActionReturnStatus> result = model.WriteAttribute(test.GetRequest(), decoder);
        ASSERT_TRUE(result.has_value() && result->IsSuccess());
    }

    EXPECT_SUCCESS(model.Registry().Unregister(&fakeClusterServer));
}

TEST_F(TestCodegenModelViaMocks, ServerClusterInterfacesRead)
{
    TestServerClusterContext testContext;

    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;

    model.SetPersistentStorageDelegate(&testContext.StorageDelegate());
    ASSERT_EQ(model.Startup(testContext.ImContext()), CHIP_NO_ERROR);

    const ConcreteClusterPath kTestClusterPath(kMockEndpoint1, MockClusterId(2));
    FakeDefaultServerCluster fakeClusterServer(kTestClusterPath);
    ServerClusterRegistration registration(fakeClusterServer);

    ASSERT_EQ(model.Registry().Register(registration), CHIP_NO_ERROR);

    // Read just works
    {

        uint32_t value;
        ASSERT_EQ(ReadU32Attribute(
                      model, { kTestClusterPath.mEndpointId, kTestClusterPath.mClusterId, kAttributeIdFakeAllowsWrite }, value),
                  CHIP_NO_ERROR);
    }

    EXPECT_SUCCESS(model.Registry().Unregister(&fakeClusterServer));
}

TEST_F(TestCodegenModelViaMocks, ServerClusterInterfacesRegistration)
{
    TestServerClusterContext testContext;

    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;

    model.SetPersistentStorageDelegate(&testContext.StorageDelegate());
    ASSERT_EQ(model.Startup(testContext.ImContext()), CHIP_NO_ERROR);

    const ConcreteClusterPath kTestClusterPath(kMockEndpoint1, MockClusterId(2));

    FakeDefaultServerCluster fakeClusterServer(kTestClusterPath);
    ServerClusterRegistration registration(fakeClusterServer);

    uint32_t revisionEmber;
    ASSERT_EQ(ReadU32Attribute(
                  model,
                  { kTestClusterPath.mEndpointId, kTestClusterPath.mClusterId, Clusters::Globals::Attributes::ClusterRevision::Id },
                  revisionEmber),
              CHIP_NO_ERROR);
    EXPECT_EQ(revisionEmber, 0u);
    static_assert(FakeDefaultServerCluster::kFakeClusterRevision != 0);

    ASSERT_EQ(model.Registry().Register(registration), CHIP_NO_ERROR);

    uint32_t revision;
    ASSERT_EQ(ReadU32Attribute(
                  model,
                  { kTestClusterPath.mEndpointId, kTestClusterPath.mClusterId, Clusters::Globals::Attributes::ClusterRevision::Id },
                  revision),
              CHIP_NO_ERROR);
    EXPECT_EQ(revision, FakeDefaultServerCluster::kFakeClusterRevision);

    // now that registration looks ok and DIFFERENT from ember, invoke various methods on the registered cluster
    // to ensure behavior is redirected correctly
    {
        ReadOnlyBufferBuilder<AttributeEntry> builder;
        ASSERT_EQ(model.Attributes(kTestClusterPath, builder), CHIP_NO_ERROR);

        ReadOnlyBufferBuilder<AttributeEntry> expectedEntriesBuilder;
        ASSERT_EQ(expectedEntriesBuilder.AppendElements(FakeDefaultServerCluster::kExtraAttributes), CHIP_NO_ERROR);
        ASSERT_EQ(expectedEntriesBuilder.AppendElements(DefaultServerCluster::GlobalAttributes()), CHIP_NO_ERROR);

        // Attributes will be just global attributes
        ASSERT_TRUE(expectedEntriesBuilder.TakeBuffer().data_equal(builder.TakeBuffer()));
    }

    {
        ReadOnlyBufferBuilder<AcceptedCommandEntry> builder;
        ASSERT_EQ(model.AcceptedCommands(kTestClusterPath, builder), CHIP_NO_ERROR);
        ASSERT_TRUE(Span<const AcceptedCommandEntry>(FakeDefaultServerCluster::kAcceptedCommands).data_equal(builder.TakeBuffer()));
    }

    {
        ReadOnlyBufferBuilder<CommandId> builder;
        ASSERT_EQ(model.GeneratedCommands(kTestClusterPath, builder), CHIP_NO_ERROR);
        ASSERT_TRUE(Span<const CommandId>(FakeDefaultServerCluster::kGeneratedCommands).data_equal(builder.TakeBuffer()));
    }

    // Invoke specifically on the fake server returns a unique (and non-spec really) error
    // so we can see the right method is called.
    {
        const ConcreteCommandPath kCommandPath(kTestClusterPath.mEndpointId, kTestClusterPath.mClusterId, kMockCommandId1);
        const InvokeRequest kInvokeRequest{ .path = kCommandPath };
        chip::TLV::TLVReader tlvReader;

        // Using a handler set to nullptr as it is not used by the impl
        std::optional<ActionReturnStatus> result = model.InvokeCommand(kInvokeRequest, tlvReader, /* handler = */ nullptr);
        ASSERT_TRUE(result.has_value() && result->GetUnderlyingError() == CHIP_ERROR_INCORRECT_STATE);
    }

    // Write attribute also has a specific error to know the right code is called
    {
        WriteOperation test(kTestClusterPath.mEndpointId, kTestClusterPath.mClusterId, kAttributeIdTimedWrite);
        test.SetSubjectDescriptor(kAdminSubjectDescriptor);

        AttributeValueDecoder decoder = test.DecoderFor<uint32_t>(1234);

        std::optional<ActionReturnStatus> result = model.WriteAttribute(test.GetRequest(), decoder);
        ASSERT_TRUE(result.has_value() && result->GetUnderlyingError() == CHIP_ERROR_INCORRECT_STATE);
    }

    EXPECT_SUCCESS(model.Registry().Unregister(&fakeClusterServer));
}

TEST_F(TestCodegenModelViaMocks, EventInfo)
{
    // Test that we format the event info correctly
    TestServerClusterContext testContext;
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;

    // Mock models always set event privilege to admin.
    EventEntry entry;
    ASSERT_EQ(model.EventInfo({ kMockEndpoint1, MockClusterId(1), kTestEventId }, entry), CHIP_NO_ERROR);
    ASSERT_EQ(entry.readPrivilege, Access::Privilege::kAdminister);
    ASSERT_EQ(model.EventInfo({ kMockEndpoint1, MockClusterId(2), kTestEventId }, entry), CHIP_NO_ERROR);
    ASSERT_EQ(entry.readPrivilege, Access::Privilege::kAdminister);

    const ConcreteClusterPath kTestClusterPath(kMockEndpoint1, MockClusterId(2));
    FakeDefaultServerCluster fakeClusterServer(kTestClusterPath);
    ServerClusterRegistration registration(fakeClusterServer);
    ASSERT_EQ(model.Registry().Register(registration), CHIP_NO_ERROR);

    fakeClusterServer.mEventInfoFakePrivilege = Access::Privilege::kOperate;
    ASSERT_EQ(model.EventInfo({ kMockEndpoint1, MockClusterId(2), kTestEventId }, entry), CHIP_NO_ERROR);
    ASSERT_EQ(entry.readPrivilege, Access::Privilege::kOperate);

    fakeClusterServer.mEventInfoFakePrivilege = Access::Privilege::kView;
    ASSERT_EQ(model.EventInfo({ kMockEndpoint1, MockClusterId(2), kTestEventId }, entry), CHIP_NO_ERROR);
    ASSERT_EQ(entry.readPrivilege, Access::Privilege::kView);

    // the other cluster is unchanged
    ASSERT_EQ(model.EventInfo({ kMockEndpoint1, MockClusterId(1), kTestEventId }, entry), CHIP_NO_ERROR);
    ASSERT_EQ(entry.readPrivilege, Access::Privilege::kAdminister);

    EXPECT_SUCCESS(model.Registry().Unregister(&fakeClusterServer));

    // once unregistered, go back to the default
    ASSERT_EQ(model.EventInfo({ kMockEndpoint1, MockClusterId(2), kTestEventId }, entry), CHIP_NO_ERROR);
    ASSERT_EQ(entry.readPrivilege, Access::Privilege::kAdminister);
}

TEST_F(TestCodegenModelViaMocks, ServerClusterInterfacesListClusters)
{
    TestServerClusterContext testContext;

    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;

    model.SetPersistentStorageDelegate(&testContext.StorageDelegate());
    ASSERT_EQ(model.Startup(testContext.ImContext()), CHIP_NO_ERROR);

    // will register a fake cluster server which overrides the cluster data version
    // once registered
    const ConcreteClusterPath kTestClusterPath(kMockEndpoint1, MockClusterId(2));
    FakeDefaultServerCluster fakeClusterServer(kTestClusterPath);
    ServerClusterRegistration registration(fakeClusterServer);

    // ensure ember and fake server do not ramdomly point to the same data
    // version as we use this data version to differentiate between the two
    DataVersion * versionPtr = emberAfDataVersionStorage(kTestClusterPath);
    ASSERT_NE(versionPtr, nullptr);
    if (*versionPtr == fakeClusterServer.GetDataVersion(kTestClusterPath))
    {
        fakeClusterServer.TestIncreaseDataVersion();
    }

    ReadOnlyBufferBuilder<DataModel::ServerClusterEntry> builder;

    ASSERT_EQ(model.ServerClusters(kTestClusterPath.mEndpointId, builder), CHIP_NO_ERROR);
    std::vector<ServerClusterEntry> originalClusters;
    for (auto entry : builder.TakeBuffer())
    {
        originalClusters.push_back(entry);
    }

    ASSERT_EQ(model.Registry().Register(registration), CHIP_NO_ERROR);

    ASSERT_EQ(model.ServerClusters(kTestClusterPath.mEndpointId, builder), CHIP_NO_ERROR);
    std::vector<ServerClusterEntry> afterRegistrationClusters;
    for (auto entry : builder.TakeBuffer())
    {
        afterRegistrationClusters.push_back(entry);
    }

    // lists MUST be identical EXCEPT data version will be different
    // EMBER has one data version, clusters have a random (different) one
    EXPECT_EQ(originalClusters.size(), afterRegistrationClusters.size());
    std::sort(originalClusters.begin(), originalClusters.end(),
              [](const ServerClusterEntry & a, const ServerClusterEntry & b) { return a.clusterId < b.clusterId; });
    std::sort(afterRegistrationClusters.begin(), afterRegistrationClusters.end(),
              [](const ServerClusterEntry & a, const ServerClusterEntry & b) { return a.clusterId < b.clusterId; });

    bool updatedClusterFound = false;

    for (size_t i = 0; i < originalClusters.size(); i++)
    {
        const ServerClusterEntry & original   = originalClusters[i];
        const ServerClusterEntry & registered = afterRegistrationClusters[i];

        if (original.clusterId == kTestClusterPath.mClusterId)
        {
            updatedClusterFound = true;
            EXPECT_EQ(registered.clusterId, original.clusterId);
            EXPECT_EQ(registered.dataVersion, fakeClusterServer.GetDataVersion(kTestClusterPath));
            EXPECT_EQ(registered.flags, fakeClusterServer.GetClusterFlags(kTestClusterPath));

            // version MUST be different for ember for the test to make sense
            EXPECT_NE(original.dataVersion, registered.dataVersion);
        }
        else
        {
            EXPECT_EQ(original.clusterId, registered.clusterId);
            EXPECT_EQ(original.dataVersion, registered.dataVersion);
            EXPECT_EQ(original.flags, registered.flags);
        }
    }
    EXPECT_TRUE(updatedClusterFound);

    EXPECT_SUCCESS(model.Registry().Unregister(&fakeClusterServer));
    EXPECT_SUCCESS(model.Shutdown());
}
#if CHIP_CONFIG_USE_ENDPOINT_UNIQUE_ID
TEST_F(TestCodegenModelViaMocks, EndpointUniqueID)
{
    UseMockNodeConfig config(gTestNodeConfig);
    CodegenDataModelProviderWithContext model;

    // Mock endpoint 1 has a unique ID
    char buffer[chip::app::Clusters::Descriptor::Attributes::EndpointUniqueID::TypeInfo::MaxLength()] = { 0 };
    MutableCharSpan span(buffer);
    // Mock endpoint 4 has a unique ID
    // ASSERT_TRUE(builder.IsEmpty()); // ownership taken above, we start fresh
    ASSERT_EQ(model.EndpointUniqueID(kMockEndpoint4, span), CHIP_NO_ERROR);
    EXPECT_TRUE(span.data_equal("AABBCCDDEEFFGGHHIIJJKKLLMMNNOO01"_span));
}
#endif
